pep8
This commit is contained in:
parent
32673becbd
commit
b26ee622da
@ -10,11 +10,12 @@ python:
|
||||
install:
|
||||
- pip install requests
|
||||
- pip install python-dateutil
|
||||
- pip install coveralls pep257 restructuredtext_lint testfixtures
|
||||
- pip install coveralls pep8 pep257 restructuredtext_lint testfixtures
|
||||
- python setup.py install
|
||||
# command to run tests
|
||||
script:
|
||||
- python setup.py test
|
||||
- pep8 --ignore=E501 resync tests bin/resync bin/resync-explorer
|
||||
- pep257 resync
|
||||
- rst-lint README
|
||||
- coverage run --source=resync setup.py test
|
||||
|
||||
101
bin/resync
101
bin/resync
@ -22,26 +22,27 @@ import sys
|
||||
|
||||
from resync import __version__
|
||||
from resync.client import Client, ClientFatalError
|
||||
from resync.client_utils import init_logging,count_true_args,parse_links,parse_capabilities,parse_capability_lists
|
||||
from resync.client_utils import init_logging, count_true_args, parse_links, parse_capabilities, parse_capability_lists
|
||||
|
||||
DEFAULT_LOGFILE = 'resync-client.log'
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
if (sys.version_info < (2,6)):
|
||||
if (sys.version_info < (2, 6)):
|
||||
sys.exit("This program requires python version 2.6 or later")
|
||||
|
||||
|
||||
# Options and arguments
|
||||
p = optparse.OptionParser(description='ResourceSync command line client',
|
||||
usage='usage: %prog [options] uri_path local_path (-h for help)',
|
||||
version='%prog '+__version__ )
|
||||
version='%prog ' + __version__)
|
||||
|
||||
# Modes
|
||||
# a. modes using remote sitemap/resources
|
||||
rem = p.add_option_group('REMOTE MODES',
|
||||
'These modes use a remote source that is specified in a set of uri=path mappings '
|
||||
'and potentially also using an explicit --sitemap location. The default mode is '
|
||||
'--baseline. See also: resync-explorer for an interactive client.')
|
||||
'These modes use a remote source that is specified in a set of uri=path mappings '
|
||||
'and potentially also using an explicit --sitemap location. The default mode is '
|
||||
'--baseline. See also: resync-explorer for an interactive client.')
|
||||
rem.add_option('--baseline', '-b', action='store_true',
|
||||
help='baseline sync of resources from remote source (src) to local filesystem (dst)')
|
||||
rem.add_option('--incremental', '--inc', '-i', action='store_true',
|
||||
@ -52,7 +53,7 @@ def main():
|
||||
help="parse a remote sitemap/sitemapindex (from mapping or explicit --sitemap) and show summary information including document type and number of entries")
|
||||
# b. modes based solely on files on local disk
|
||||
loc = p.add_option_group('LOCAL MODES',
|
||||
'These modes act on files on the local disk')
|
||||
'These modes act on files on the local disk')
|
||||
loc.add_option('--resourcelist', '--resource-list', '-r', action='store_true',
|
||||
help="write a resource list based on files on disk using uri=path mappings "
|
||||
"in reverse to calculate URIs from the local paths. Scans local disk "
|
||||
@ -80,7 +81,8 @@ def main():
|
||||
help="write a Resource Dump. Specify output file with --outfile and use other "
|
||||
"options as for --changelist")
|
||||
|
||||
# Specification of map between remote URI and local file paths, and remote sitemap
|
||||
# Specification of map between remote URI and local file paths, and remote
|
||||
# sitemap
|
||||
nam = p.add_option_group('FILE/URI NAMING OPTIONS')
|
||||
nam.add_option('--outfile', type=str, action='store',
|
||||
help="write output to specified file rather than STDOUT or default")
|
||||
@ -94,7 +96,7 @@ def main():
|
||||
help="reference sitemap name for --changelist calculation")
|
||||
nam.add_option('--newreference', type=str, action='store',
|
||||
help="updated reference sitemap name for --changelist calculation")
|
||||
nam.add_option('--changelist-uri','--change-list-uri', type=str, action='store',
|
||||
nam.add_option('--changelist-uri', '--change-list-uri', type=str, action='store',
|
||||
help="explicitly set the changelist URI that will be use in --inc mode, "
|
||||
"overrides process of getting this from the sitemap")
|
||||
|
||||
@ -105,16 +107,16 @@ def main():
|
||||
"(repeat option for multiple links)")
|
||||
lks.add_option('--describedby-link', type=str, action='store',
|
||||
help="add an <rs:md rel=\"describedby\" link to "
|
||||
"a description of the feed at the URI given")
|
||||
lks.add_option('--sourcedescription-link', '--source-description-link',
|
||||
"a description of the feed at the URI given")
|
||||
lks.add_option('--sourcedescription-link', '--source-description-link',
|
||||
type=str, action='store',
|
||||
help="for a Capability List add a <rs:md rel=\"up\" link to the"
|
||||
"Source Description document at the URI given, else ignored")
|
||||
"Source Description document at the URI given, else ignored")
|
||||
lks.add_option('--capabilitylist-link', '--capability-list-link',
|
||||
type=str, action='store',
|
||||
help="for all documents except a Capability List or a "
|
||||
"Source Description, add an <rs:md rel=\"up\" link "
|
||||
"to the Capability List at the URI given")
|
||||
"to the Capability List at the URI given")
|
||||
|
||||
# Options that apply to multiple modes
|
||||
opt = p.add_option_group('MISCELANEOUS OPTIONS')
|
||||
@ -169,65 +171,65 @@ def main():
|
||||
|
||||
# Implement exclusive arguments and default --baseline (support for exclusive
|
||||
# groups in argparse is incomplete is python2.6)
|
||||
if (not args.baseline and not args.incremental and not args.audit and
|
||||
not args.parse and not args.resourcelist and not args.changelist and
|
||||
not args.capabilitylist and not args.sourcedescription and
|
||||
not args.resourcedump and not args.changedump):
|
||||
if (len(map)==0):
|
||||
if (not args.baseline and not args.incremental and not args.audit and
|
||||
not args.parse and not args.resourcelist and not args.changelist and
|
||||
not args.capabilitylist and not args.sourcedescription and
|
||||
not args.resourcedump and not args.changedump):
|
||||
if (len(map) == 0):
|
||||
# No args at all, show help
|
||||
p.print_help()
|
||||
return
|
||||
else:
|
||||
args.baseline=True
|
||||
elif (count_true_args(args.baseline,args.incremental,args.audit,args.parse,
|
||||
args.resourcelist,args.changelist,args.capabilitylist,
|
||||
args.sourcedescription,args.resourcedump,args.changedump)>1):
|
||||
args.baseline = True
|
||||
elif (count_true_args(args.baseline, args.incremental, args.audit, args.parse,
|
||||
args.resourcelist, args.changelist, args.capabilitylist,
|
||||
args.sourcedescription, args.resourcedump, args.changedump) > 1):
|
||||
p.error("Only one of --baseline, --incremental, --audit, --parse, --resourcelist, --changelist, --capabilitylist, --sourcedescription, --resourcedump, --changedump modes allowed")
|
||||
|
||||
# Configure logging module and create logger instance
|
||||
init_logging( to_file=args.logger, logfile=args.logfile, default_logfile=DEFAULT_LOGFILE,
|
||||
verbose=args.verbose, eval_mode=args.eval )
|
||||
init_logging(to_file=args.logger, logfile=args.logfile, default_logfile=DEFAULT_LOGFILE,
|
||||
verbose=args.verbose, eval_mode=args.eval)
|
||||
|
||||
c = Client( checksum=args.checksum,
|
||||
verbose=args.verbose,
|
||||
dryrun=args.dryrun )
|
||||
c = Client(checksum=args.checksum,
|
||||
verbose=args.verbose,
|
||||
dryrun=args.dryrun)
|
||||
|
||||
try:
|
||||
if (map):
|
||||
# Mappings apply to (almost) everything
|
||||
c.set_mappings(map)
|
||||
if (args.sitemap):
|
||||
c.sitemap_name=args.sitemap
|
||||
c.sitemap_name = args.sitemap
|
||||
if (args.warc):
|
||||
c.dump_format='warc'
|
||||
c.dump_format = 'warc'
|
||||
if (args.exclude):
|
||||
c.exclude_patterns=args.exclude
|
||||
c.exclude_patterns = args.exclude
|
||||
if (args.multifile):
|
||||
c.allow_multifile=not args.multifile
|
||||
c.allow_multifile = not args.multifile
|
||||
if (args.noauth):
|
||||
c.noauth=args.noauth
|
||||
c.noauth = args.noauth
|
||||
if (args.strictauth):
|
||||
c.strictauth=args.strictauth
|
||||
c.strictauth = args.strictauth
|
||||
if (args.max_sitemap_entries):
|
||||
c.max_sitemap_entries=args.max_sitemap_entries
|
||||
c.max_sitemap_entries = args.max_sitemap_entries
|
||||
if (args.ignore_failures):
|
||||
c.ignore_failures=args.ignore_failures
|
||||
c.ignore_failures = args.ignore_failures
|
||||
|
||||
# Links apply to anything that writes sitemaps
|
||||
links = parse_links(args.link)
|
||||
# Add specific links is appropriate cases
|
||||
if (args.capabilitylist_link and
|
||||
not args.capabilitylist and
|
||||
not args.sourcedescription):
|
||||
# rel="up" to Capability List in all but Capability List
|
||||
if (args.capabilitylist_link and
|
||||
not args.capabilitylist and
|
||||
not args.sourcedescription):
|
||||
# rel="up" to Capability List in all but Capability List
|
||||
# and Source Description
|
||||
links.insert(0,{'rel':'up','href':args.capabilitylist_link})
|
||||
links.insert(0, {'rel': 'up', 'href': args.capabilitylist_link})
|
||||
if (args.sourcedescription_link and args.capabilitylist):
|
||||
# rel="up" to Source Description from Capability List
|
||||
links.insert(0,{'rel':'up','href':args.sourcedescription_link})
|
||||
links.insert(0, {'rel': 'up', 'href': args.sourcedescription_link})
|
||||
if (args.describedby_link):
|
||||
links.insert(0,{'rel':'describedby','href':args.describedby_link})
|
||||
|
||||
links.insert(0, {'rel': 'describedby',
|
||||
'href': args.describedby_link})
|
||||
|
||||
# Finally, do something...
|
||||
if (args.baseline or args.audit):
|
||||
@ -246,9 +248,11 @@ def main():
|
||||
dump=args.resourcedump)
|
||||
elif (args.changelist or args.changedump):
|
||||
if (not args.reference and not args.empty):
|
||||
p.error("Must supply --reference sitemap for --changelist, or --empty")
|
||||
p.error(
|
||||
"Must supply --reference sitemap for --changelist, or --empty")
|
||||
c.write_change_list(ref_sitemap=args.reference,
|
||||
newref_sitemap=( args.newreference if (args.newreference) else None ),
|
||||
newref_sitemap=(args.newreference if (
|
||||
args.newreference) else None),
|
||||
empty=args.empty,
|
||||
paths=args.paths,
|
||||
outfile=args.outfile,
|
||||
@ -261,12 +265,13 @@ def main():
|
||||
links=links)
|
||||
elif (args.sourcedescription):
|
||||
c.write_source_description(
|
||||
capability_lists=parse_capability_lists(args.sourcedescription),
|
||||
capability_lists=parse_capability_lists(
|
||||
args.sourcedescription),
|
||||
outfile=args.outfile,
|
||||
links=links)
|
||||
else:
|
||||
p.error("Unknown mode requested")
|
||||
# Any problem we expect will come as a ClientFatalError, anything else
|
||||
# Any problem we expect will come as a ClientFatalError, anything else
|
||||
# is... an exception ;-)
|
||||
except ClientFatalError as e:
|
||||
sys.stderr.write("\nFatalError: " + str(e) + "\n")
|
||||
|
||||
@ -27,17 +27,19 @@ from resync.explorer import Explorer
|
||||
|
||||
DEFAULT_LOGFILE = 'resync-explorer.log'
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
if (sys.version_info < (2,6)):
|
||||
if (sys.version_info < (2, 6)):
|
||||
sys.exit("This program requires python version 2.6 or later")
|
||||
|
||||
|
||||
# Options and arguments
|
||||
p = optparse.OptionParser(description='ResourceSync explorer',
|
||||
usage='usage: %prog [options] uri_path local_path (-h for help)',
|
||||
version='%prog '+__version__ )
|
||||
version='%prog ' + __version__)
|
||||
|
||||
# Specification of map between remote URI and local file paths, and remote sitemap
|
||||
# Specification of map between remote URI and local file paths, and remote
|
||||
# sitemap
|
||||
nam = p.add_option_group('FILE/URI NAMING OPTIONS')
|
||||
nam.add_option('--outfile', type=str, action='store',
|
||||
help="write sitemap to specified file rather than STDOUT")
|
||||
@ -53,7 +55,7 @@ def main():
|
||||
help="updated reference sitemap name for --changelist calculation")
|
||||
nam.add_option('--dump', metavar='DUMPFILE', type=str, action='store',
|
||||
help="write dump to specified file for --resourcelist or --changelist")
|
||||
nam.add_option('--changelist-uri','--change-list-uri', type=str, action='store',
|
||||
nam.add_option('--changelist-uri', '--change-list-uri', type=str, action='store',
|
||||
help="explicitly set the changelist URI that will be use in --inc mode, "
|
||||
"overrides process of getting this from the sitemap")
|
||||
|
||||
@ -88,31 +90,31 @@ def main():
|
||||
|
||||
(args, map) = p.parse_args()
|
||||
|
||||
init_logging( to_file=args.logger, logfile=args.logfile, default_logfile=DEFAULT_LOGFILE,
|
||||
verbose=args.verbose )
|
||||
init_logging(to_file=args.logger, logfile=args.logfile, default_logfile=DEFAULT_LOGFILE,
|
||||
verbose=args.verbose)
|
||||
|
||||
print("----- ResourceSync Explorer -----")
|
||||
c = Explorer( checksum=args.checksum,
|
||||
verbose=args.verbose )
|
||||
c = Explorer(checksum=args.checksum,
|
||||
verbose=args.verbose)
|
||||
|
||||
try:
|
||||
if (map):
|
||||
# Mappings apply to (almost) everything
|
||||
c.set_mappings(map)
|
||||
if (args.sitemap):
|
||||
c.sitemap_name=args.sitemap
|
||||
c.sitemap_name = args.sitemap
|
||||
if (args.exclude):
|
||||
c.exclude_patterns=args.exclude
|
||||
c.exclude_patterns = args.exclude
|
||||
if (args.multifile):
|
||||
c.allow_multifile=not args.multifile
|
||||
c.allow_multifile = not args.multifile
|
||||
if (args.noauth):
|
||||
c.noauth=args.noauth
|
||||
c.noauth = args.noauth
|
||||
if (args.max_sitemap_entries):
|
||||
c.max_sitemap_entries=args.max_sitemap_entries
|
||||
c.max_sitemap_entries = args.max_sitemap_entries
|
||||
|
||||
c.explore()
|
||||
|
||||
# Any problem we expect will come as a ClientFatalError, anything else
|
||||
# Any problem we expect will come as a ClientFatalError, anything else
|
||||
# is... an exception ;-)
|
||||
except ClientFatalError as e:
|
||||
sys.stderr.write("\nFatalError: " + str(e) + "\n")
|
||||
|
||||
@ -11,6 +11,5 @@ from resync.change_list import ChangeList
|
||||
from resync.resource_dump import ResourceDump
|
||||
from resync.resource_dump_manifest import ResourceDumpManifest
|
||||
from resync.change_dump import ChangeDump
|
||||
from resync.archives import ResourceListArchive,ResourceDumpArchive,ChangeListArchive,ChangeDumpArchive
|
||||
from resync.archives import ResourceListArchive, ResourceDumpArchive, ChangeListArchive, ChangeDumpArchive
|
||||
from resync.resource import Resource
|
||||
|
||||
|
||||
@ -2,22 +2,23 @@
|
||||
|
||||
The ResourceSync Archives specification
|
||||
http://www.openarchives.org/rs/archives specifies capabilities
|
||||
that provide archives of the 4 core capabilities. While some
|
||||
that provide archives of the 4 core capabilities. While some
|
||||
optional attributes are different the basic structure is the
|
||||
same for the Resource List Archive, Change List Archive,
|
||||
Resource Dump Archive, and Change Dump Archive.
|
||||
same for the Resource List Archive, Change List Archive,
|
||||
Resource Dump Archive, and Change Dump Archive.
|
||||
"""
|
||||
|
||||
import collections
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import URLopener
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import URLopener
|
||||
|
||||
from .list_base_with_index import ListBaseWithIndex
|
||||
from .resource import Resource
|
||||
from .sitemap import Sitemap
|
||||
|
||||
|
||||
class ResourceListArchive(ListBaseWithIndex):
|
||||
"""Class representing an Resource List Archive."""
|
||||
|
||||
@ -30,6 +31,7 @@ class ResourceListArchive(ListBaseWithIndex):
|
||||
super(ResourceListArchive, self).__init__(resources=resources, md=md, ln=ln, uri=uri,
|
||||
capability_name='resourcelist-archive')
|
||||
|
||||
|
||||
class ChangeListArchive(ListBaseWithIndex):
|
||||
"""Class representing an Change List Archive."""
|
||||
|
||||
@ -39,6 +41,7 @@ class ChangeListArchive(ListBaseWithIndex):
|
||||
super(ChangeListArchive, self).__init__(resources=resources, md=md, ln=ln, uri=uri,
|
||||
capability_name='changelist-archive')
|
||||
|
||||
|
||||
class ResourceDumpArchive(ListBaseWithIndex):
|
||||
"""Class representing an Resource Dump Archive."""
|
||||
|
||||
@ -49,6 +52,7 @@ class ResourceDumpArchive(ListBaseWithIndex):
|
||||
capability_name='resourcedump-archive',
|
||||
resources_class=resources_class)
|
||||
|
||||
|
||||
class ChangeDumpArchive(ListBaseWithIndex):
|
||||
"""Class representing an Change Dump Archive."""
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""ResourceSync Capability List object.
|
||||
|
||||
An Capability List is a set of capabilitys with some metadata for
|
||||
each capability. The Capability List object may also contain metadata
|
||||
An Capability List is a set of capabilitys with some metadata for
|
||||
each capability. The Capability List object may also contain metadata
|
||||
and links like other lists.
|
||||
"""
|
||||
|
||||
@ -12,26 +12,27 @@ from .resource_set import ResourceSet
|
||||
from .list_base import ListBase
|
||||
from .sitemap import Sitemap
|
||||
|
||||
|
||||
class CapabilitySet(ResourceSet):
|
||||
"""Class for storage of resources in a Capability List.
|
||||
|
||||
Extends the ResourceSet to add checks to ensure that there are
|
||||
never two entries for the same resource, and that values are
|
||||
Extends the ResourceSet to add checks to ensure that there are
|
||||
never two entries for the same resource, and that values are
|
||||
returned in the canonical order.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize CpabilitySet."""
|
||||
self.order = [ 'resourcelist', 'resourcedump',
|
||||
'changelist', 'changedump',
|
||||
'resourcelist-archive', 'resourcedump-archive',
|
||||
'changelist-archive', 'changedump-archive' ]
|
||||
self.order = ['resourcelist', 'resourcedump',
|
||||
'changelist', 'changedump',
|
||||
'resourcelist-archive', 'resourcedump-archive',
|
||||
'changelist-archive', 'changedump-archive']
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterator over all the resources in capability order.
|
||||
|
||||
Deals with the case of unknown capabilities or duplicate entries
|
||||
by using uri order for duplicates and adding any unknown ones
|
||||
by using uri order for duplicates and adding any unknown ones
|
||||
at the end
|
||||
"""
|
||||
self._iter_next_list = []
|
||||
@ -40,7 +41,7 @@ class CapabilitySet(ResourceSet):
|
||||
for uri in self.keys():
|
||||
cap = self[uri].capability
|
||||
if (cap not in uris):
|
||||
uris[cap]=[]
|
||||
uris[cap] = []
|
||||
uris[cap].append(uri)
|
||||
# build list or uris in defined order for iterator
|
||||
for cap in self.order:
|
||||
@ -48,7 +49,8 @@ class CapabilitySet(ResourceSet):
|
||||
for uri in sorted(uris[cap]):
|
||||
self._iter_next_list.append(uri)
|
||||
del uris[cap]
|
||||
# add any left over capabilities we don't know about in alphabetical order
|
||||
# add any left over capabilities we don't know about in alphabetical
|
||||
# order
|
||||
for cap in uris:
|
||||
for uri in sorted(uris[cap]):
|
||||
self._iter_next_list.append(uri)
|
||||
@ -56,7 +58,7 @@ class CapabilitySet(ResourceSet):
|
||||
return(iter(self._iter_next, None))
|
||||
|
||||
def _iter_next(self):
|
||||
if (len(self._iter_next_list)>0):
|
||||
if (len(self._iter_next_list) > 0):
|
||||
return(self[self._iter_next_list.pop()])
|
||||
else:
|
||||
return(None)
|
||||
@ -65,7 +67,7 @@ class CapabilitySet(ResourceSet):
|
||||
class CapabilityList(ListBase):
|
||||
"""Class representing a Capability List.
|
||||
|
||||
An Capability List will admit only one resource with any given
|
||||
An Capability List will admit only one resource with any given
|
||||
URI. The iterator over resources is expected to return them in
|
||||
canonical order of capability names as defined in main specification
|
||||
section 7 and archives specification section 6.
|
||||
@ -88,15 +90,15 @@ class CapabilityList(ListBase):
|
||||
"""
|
||||
if isinstance(resource, collections.Iterable):
|
||||
for r in resource:
|
||||
self.resources.add(r,replace)
|
||||
self.resources.add(r, replace)
|
||||
else:
|
||||
self.resources.add(resource,replace)
|
||||
self.resources.add(resource, replace)
|
||||
|
||||
def add_capability(self,capability=None,uri=None,name=None):
|
||||
def add_capability(self, capability=None, uri=None, name=None):
|
||||
"""Specific add function for capabilities.
|
||||
|
||||
|
||||
Takes either:
|
||||
- a capability object (derived from ListBase) as the first argument
|
||||
- a capability object (derived from ListBase) as the first argument
|
||||
from which the capability name is extracted, and the URI if given
|
||||
- or a plain name string
|
||||
and
|
||||
@ -105,14 +107,14 @@ class CapabilityList(ListBase):
|
||||
if (capability is not None):
|
||||
name = capability.capability_name
|
||||
if (capability.uri is not None):
|
||||
uri=capability.uri
|
||||
self.add( Resource(uri=uri,capability=name) )
|
||||
uri = capability.uri
|
||||
self.add(Resource(uri=uri, capability=name))
|
||||
|
||||
def has_capability(self,name=None):
|
||||
def has_capability(self, name=None):
|
||||
"""True if the Capability List includes the named capability."""
|
||||
return( self.capability_info(name) is not None )
|
||||
return(self.capability_info(name) is not None)
|
||||
|
||||
def capability_info(self,name=None):
|
||||
def capability_info(self, name=None):
|
||||
"""Return information about the requested capability from this list.
|
||||
|
||||
Will return None if there is no information about the requested capability.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""ResourceSync Change Dump object.
|
||||
|
||||
A Change Dump is a set of content dump package resources
|
||||
with some metadata for each resource.
|
||||
with some metadata for each resource.
|
||||
|
||||
The Change Dump may also contain metadata and links like
|
||||
other ResourceSync documents.
|
||||
@ -12,6 +12,7 @@ http://www.openarchives.org/rs/resourcesync#ChangeDump
|
||||
|
||||
from .resource_list import ResourceList
|
||||
|
||||
|
||||
class ChangeDump(ResourceList):
|
||||
"""Class representing an Change Dump.
|
||||
|
||||
@ -20,7 +21,8 @@ class ChangeDump(ResourceList):
|
||||
and implemented as a sub-class of ResourceList.
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, md=None, ln=None, uri=None, allow_multifile=None, mapper=None):
|
||||
def __init__(self, resources=None, md=None, ln=None,
|
||||
uri=None, allow_multifile=None, mapper=None):
|
||||
"""Initialize ChangeDump.
|
||||
|
||||
Simply sets capability_name to 'changedump' when
|
||||
@ -28,4 +30,4 @@ class ChangeDump(ResourceList):
|
||||
"""
|
||||
super(ChangeDump, self).__init__(resources=resources, md=md, ln=ln, uri=uri,
|
||||
mapper=mapper)
|
||||
self.capability_name='changedump'
|
||||
self.capability_name = 'changedump'
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"""ResourceSync ChangeDumpManifest object.
|
||||
|
||||
A ChangeDumpManifest lists the set of files/resources included
|
||||
within a content package that is included in a ChangeDump.
|
||||
A ChangeDumpManifest lists the set of files/resources included
|
||||
within a content package that is included in a ChangeDump.
|
||||
|
||||
The ChangeeDumpManifest object will include the change type,
|
||||
a path for each item (except in the case of "deleted"), and may
|
||||
a path for each item (except in the case of "deleted"), and may
|
||||
also contain metadata and links.
|
||||
|
||||
Described in specification at:
|
||||
@ -13,6 +13,7 @@ http://www.openarchives.org/rs/resourcesync#ChangeDumpManifest
|
||||
|
||||
from .change_list import ChangeList
|
||||
|
||||
|
||||
class ChangeDumpManifest(ChangeList):
|
||||
"""Class representing a Change Dump Manifest.
|
||||
|
||||
@ -21,11 +22,14 @@ class ChangeDumpManifest(ChangeList):
|
||||
and implemented as a sub-class of ChangeeList.
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, md=None, ln=None, uri=None, allow_multifile=None, mapper=None):
|
||||
def __init__(self, resources=None, md=None, ln=None,
|
||||
uri=None, allow_multifile=None, mapper=None):
|
||||
"""Initialize ChangeDumpManifest.
|
||||
|
||||
Simply sets capability_name to 'changedump-manifest' when
|
||||
subclassing ChangeList.
|
||||
"""
|
||||
super(ChangeDumpManifest, self).__init__(resources=resources, md=md, ln=ln, uri=uri, mapper=mapper)
|
||||
super(ChangeDumpManifest, self).__init__(
|
||||
resources=resources, md=md, ln=ln,
|
||||
uri=uri, mapper=mapper)
|
||||
self.capability_name = 'changedump-manifest'
|
||||
|
||||
@ -8,23 +8,24 @@ are Resource objects.
|
||||
|
||||
Different from an resource_list, a change_list may include multiple
|
||||
descriptions for the same resource. The change_list is ordered
|
||||
from first entry to last entry.
|
||||
from first entry to last entry.
|
||||
|
||||
Different from an resource_list, dereference by a URI yields a
|
||||
ChangeList containing descriptions pertaining to that
|
||||
Different from an resource_list, dereference by a URI yields a
|
||||
ChangeList containing descriptions pertaining to that
|
||||
particular resource.
|
||||
"""
|
||||
|
||||
import collections
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import URLopener
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import URLopener
|
||||
|
||||
from .list_base_with_index import ListBaseWithIndex
|
||||
from .resource import Resource,ChangeTypeError
|
||||
from .resource import Resource, ChangeTypeError
|
||||
from .sitemap import Sitemap
|
||||
|
||||
|
||||
class ChangeList(ListBaseWithIndex):
|
||||
"""Class representing an Change List."""
|
||||
|
||||
@ -38,14 +39,14 @@ class ChangeList(ListBaseWithIndex):
|
||||
def add_if_changed(self, resource):
|
||||
"""Add resource if change is not None else ChangeTypeError."""
|
||||
if (resource.change is not None):
|
||||
self.resources.append(resource)
|
||||
self.resources.append(resource)
|
||||
else:
|
||||
raise ChangeTypeError(resource.change)
|
||||
|
||||
def add(self, resource):
|
||||
"""Add a resource change or an iterable collection of them.
|
||||
|
||||
Allows multiple resource_change objects for the same
|
||||
|
||||
Allows multiple resource_change objects for the same
|
||||
resource (ie. URI) and preserves the order of addition.
|
||||
"""
|
||||
if isinstance(resource, collections.Iterable):
|
||||
@ -57,9 +58,9 @@ class ChangeList(ListBaseWithIndex):
|
||||
def add_changed_resources(self, resources, change=None):
|
||||
"""Add items from a ResourceContainer resources.
|
||||
|
||||
If change is specified then the attribute is set in the Resource
|
||||
If change is specified then the attribute is set in the Resource
|
||||
objects created.
|
||||
"""
|
||||
for resource in resources:
|
||||
rc = Resource( resource=resource, change=change )
|
||||
rc = Resource(resource=resource, change=change)
|
||||
self.add(rc)
|
||||
|
||||
456
resync/client.py
456
resync/client.py
@ -1,15 +1,15 @@
|
||||
"""ResourceSync client implementation."""
|
||||
|
||||
import sys
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import urlretrieve
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import urlretrieve
|
||||
from urlparse import urlparse, urlunparse
|
||||
import os.path
|
||||
import datetime
|
||||
import distutils.dir_util
|
||||
import distutils.dir_util
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
@ -29,7 +29,8 @@ from .utils import compute_md5_for_file
|
||||
from .client_state import ClientState
|
||||
from .client_utils import ClientFatalError, url_or_file_open
|
||||
from .list_base_with_index import ListBaseIndexError
|
||||
from .w3c_datetime import str_to_datetime,datetime_to_str
|
||||
from .w3c_datetime import str_to_datetime, datetime_to_str
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Implementation of a ResourceSync client.
|
||||
@ -65,17 +66,16 @@ class Client(object):
|
||||
self.default_resource_dump = 'resourcedump.zip'
|
||||
self.default_change_dump = 'changedump.zip'
|
||||
|
||||
|
||||
def set_mappings(self,mappings):
|
||||
def set_mappings(self, mappings):
|
||||
"""Build and set Mapper object based on input mappings."""
|
||||
self.mapper = Mapper(mappings, use_default_path=True)
|
||||
|
||||
def sitemap_uri(self,basename):
|
||||
def sitemap_uri(self, basename):
|
||||
"""Get full URI (filepath) for sitemap based on basename."""
|
||||
if (re.match(r"\w+:",basename)):
|
||||
if (re.match(r"\w+:", basename)):
|
||||
# looks like URI
|
||||
return(basename)
|
||||
elif (re.match(r"/",basename)):
|
||||
elif (re.match(r"/", basename)):
|
||||
# looks like full path
|
||||
return(basename)
|
||||
else:
|
||||
@ -97,21 +97,22 @@ class Client(object):
|
||||
|
||||
paths - override paths from mappings if specified
|
||||
|
||||
set_path - set true to set the path information for each resource
|
||||
set_path - set true to set the path information for each resource
|
||||
included. This is used to build a resource list as the basis
|
||||
for creating a dump.
|
||||
|
||||
Return ResourceList. Uses existing self.mapper settings.
|
||||
"""
|
||||
# 0. Sanity checks, parse paths is specified
|
||||
if (len(self.mapper)<1):
|
||||
raise ClientFatalError("No source to destination mapping specified")
|
||||
if (len(self.mapper) < 1):
|
||||
raise ClientFatalError(
|
||||
"No source to destination mapping specified")
|
||||
if (paths is not None):
|
||||
# Expect comma separated list of paths
|
||||
paths=paths.split(',')
|
||||
paths = paths.split(',')
|
||||
# 1. Build from disk
|
||||
rlb = ResourceListBuilder(set_md5=self.checksum,mapper=self.mapper)
|
||||
rlb.set_path=set_path
|
||||
rlb = ResourceListBuilder(set_md5=self.checksum, mapper=self.mapper)
|
||||
rlb.set_path = set_path
|
||||
try:
|
||||
rlb.add_exclude_files(self.exclude_patterns)
|
||||
rl = rlb.from_disk(paths=paths)
|
||||
@ -127,137 +128,171 @@ class Client(object):
|
||||
|
||||
def log_event(self, change):
|
||||
"""Log a Resource object as an event for automated analysis."""
|
||||
self.logger.debug( "Event: "+repr(change) )
|
||||
self.logger.debug("Event: " + repr(change))
|
||||
|
||||
def baseline_or_audit(self, allow_deletion=False, audit_only=False):
|
||||
"""Baseline synchonization or audit.
|
||||
|
||||
|
||||
Both functions implemented in this routine because audit is a prerequisite
|
||||
for a baseline sync. In the case of baseline sync the last timestamp seen
|
||||
is recorded as client state.
|
||||
"""
|
||||
action = ( 'audit' if (audit_only) else 'baseline sync' )
|
||||
self.logger.debug("Starting "+action)
|
||||
### 0. Sanity checks
|
||||
if (len(self.mapper)<1):
|
||||
raise ClientFatalError("No source to destination mapping specified")
|
||||
action = ('audit' if (audit_only) else 'baseline sync')
|
||||
self.logger.debug("Starting " + action)
|
||||
# 0. Sanity checks
|
||||
if (len(self.mapper) < 1):
|
||||
raise ClientFatalError(
|
||||
"No source to destination mapping specified")
|
||||
if (not audit_only and self.mapper.unsafe()):
|
||||
raise ClientFatalError("Source to destination mappings unsafe: %s" % str(self.mapper))
|
||||
### 1. Get inventories from both src and dst
|
||||
raise ClientFatalError(
|
||||
"Source to destination mappings unsafe: %s" % str(
|
||||
self.mapper))
|
||||
# 1. Get inventories from both src and dst
|
||||
# 1.a source resource list
|
||||
try:
|
||||
self.logger.info("Reading sitemap %s" % (self.sitemap))
|
||||
src_resource_list = ResourceList(allow_multifile=self.allow_multifile, mapper=self.mapper)
|
||||
src_resource_list = ResourceList(
|
||||
allow_multifile=self.allow_multifile, mapper=self.mapper)
|
||||
src_resource_list.read(uri=self.sitemap)
|
||||
self.logger.debug("Finished reading sitemap")
|
||||
except Exception as e:
|
||||
raise ClientFatalError("Can't read source resource list from %s (%s)" % (self.sitemap,str(e)))
|
||||
self.logger.info("Read source resource list, %d resources listed" % (len(src_resource_list)))
|
||||
if (len(src_resource_list)==0):
|
||||
raise ClientFatalError("Aborting as there are no resources to sync")
|
||||
raise ClientFatalError(
|
||||
"Can't read source resource list from %s (%s)" %
|
||||
(self.sitemap, str(e)))
|
||||
self.logger.info(
|
||||
"Read source resource list, %d resources listed" %
|
||||
(len(src_resource_list)))
|
||||
if (len(src_resource_list) == 0):
|
||||
raise ClientFatalError(
|
||||
"Aborting as there are no resources to sync")
|
||||
if (self.checksum and not src_resource_list.has_md5()):
|
||||
self.checksum=False
|
||||
self.logger.info("Not calculating checksums on destination as not present in source resource list")
|
||||
self.checksum = False
|
||||
self.logger.info(
|
||||
"Not calculating checksums on destination as not present in source resource list")
|
||||
# 1.b destination resource list mapped back to source URIs
|
||||
rlb = ResourceListBuilder(set_md5=self.checksum, mapper=self.mapper)
|
||||
dst_resource_list = rlb.from_disk()
|
||||
### 2. Compare these resource lists respecting any comparison options
|
||||
(same,updated,deleted,created)=dst_resource_list.compare(src_resource_list)
|
||||
### 3. Report status and planned actions
|
||||
self.log_status(in_sync=(len(updated)+len(deleted)+len(created)==0),
|
||||
audit=True,same=len(same),created=len(created),
|
||||
updated=len(updated),deleted=len(deleted))
|
||||
if (audit_only or len(created)+len(updated)+len(deleted)==0):
|
||||
self.logger.debug("Completed "+action)
|
||||
# 2. Compare these resource lists respecting any comparison options
|
||||
(same, updated, deleted, created) = dst_resource_list.compare(src_resource_list)
|
||||
# 3. Report status and planned actions
|
||||
self.log_status(in_sync=(len(updated) + len(deleted) + len(created) == 0),
|
||||
audit=True, same=len(same), created=len(created),
|
||||
updated=len(updated), deleted=len(deleted))
|
||||
if (audit_only or len(created) + len(updated) + len(deleted) == 0):
|
||||
self.logger.debug("Completed " + action)
|
||||
return
|
||||
### 4. Check that sitemap has authority over URIs listed
|
||||
# 4. Check that sitemap has authority over URIs listed
|
||||
if (not self.noauth):
|
||||
uauth = UrlAuthority(self.sitemap, strict=self.strictauth)
|
||||
for resource in src_resource_list:
|
||||
if (not uauth.has_authority_over(resource.uri)):
|
||||
raise ClientFatalError("Aborting as sitemap (%s) mentions resource at a location it does not have authority over (%s), override with --noauth" % (self.sitemap,resource.uri))
|
||||
### 5. Grab files to do sync
|
||||
delete_msg = (", and delete %d resources" % len(deleted)) if (allow_deletion) else ''
|
||||
self.logger.warning("Will GET %d resources%s" % (len(created)+len(updated),delete_msg))
|
||||
raise ClientFatalError(
|
||||
"Aborting as sitemap (%s) mentions resource at a location it does not have authority over (%s), override with --noauth" %
|
||||
(self.sitemap, resource.uri))
|
||||
# 5. Grab files to do sync
|
||||
delete_msg = (
|
||||
", and delete %d resources" %
|
||||
len(deleted)) if (allow_deletion) else ''
|
||||
self.logger.warning(
|
||||
"Will GET %d resources%s" %
|
||||
(len(created) + len(updated), delete_msg))
|
||||
self.last_timestamp = 0
|
||||
num_created=0
|
||||
num_updated=0
|
||||
num_deleted=0
|
||||
num_created = 0
|
||||
num_updated = 0
|
||||
num_deleted = 0
|
||||
for resource in created:
|
||||
uri = resource.uri
|
||||
filename = self.mapper.src_to_dst(uri)
|
||||
self.logger.info("created: %s -> %s" % (uri,filename))
|
||||
num_created+=self.update_resource(resource,filename,'created')
|
||||
self.logger.info("created: %s -> %s" % (uri, filename))
|
||||
num_created += self.update_resource(resource, filename, 'created')
|
||||
for resource in updated:
|
||||
uri = resource.uri
|
||||
filename = self.mapper.src_to_dst(uri)
|
||||
self.logger.info("updated: %s -> %s" % (uri,filename))
|
||||
num_updated+=self.update_resource(resource,filename,'updated')
|
||||
self.logger.info("updated: %s -> %s" % (uri, filename))
|
||||
num_updated += self.update_resource(resource, filename, 'updated')
|
||||
for resource in deleted:
|
||||
uri = resource.uri
|
||||
filename = self.mapper.src_to_dst(uri)
|
||||
num_deleted+=self.delete_resource(resource,filename,allow_deletion)
|
||||
### 6. Store last timestamp to allow incremental sync
|
||||
if (not audit_only and self.last_timestamp>0):
|
||||
ClientState().set_state(self.sitemap,self.last_timestamp)
|
||||
self.logger.info("Written last timestamp %s for incremental sync" % (datetime_to_str(self.last_timestamp)))
|
||||
### 7. Done
|
||||
self.log_status(in_sync=(len(updated)+len(deleted)+len(created)==0),
|
||||
same=len(same),created=num_created,
|
||||
updated=num_updated,deleted=num_deleted,to_delete=len(deleted))
|
||||
num_deleted += self.delete_resource(resource,
|
||||
filename, allow_deletion)
|
||||
# 6. Store last timestamp to allow incremental sync
|
||||
if (not audit_only and self.last_timestamp > 0):
|
||||
ClientState().set_state(self.sitemap, self.last_timestamp)
|
||||
self.logger.info(
|
||||
"Written last timestamp %s for incremental sync" %
|
||||
(datetime_to_str(
|
||||
self.last_timestamp)))
|
||||
# 7. Done
|
||||
self.log_status(in_sync=(len(updated) + len(deleted) + len(created) == 0),
|
||||
same=len(same), created=num_created,
|
||||
updated=num_updated, deleted=num_deleted, to_delete=len(deleted))
|
||||
self.logger.debug("Completed %s" % (action))
|
||||
|
||||
def incremental(self, allow_deletion=False, change_list_uri=None, from_datetime=None):
|
||||
def incremental(self, allow_deletion=False,
|
||||
change_list_uri=None, from_datetime=None):
|
||||
"""Incremental synchronization.
|
||||
|
||||
Use Change List to do incremental sync
|
||||
"""
|
||||
self.logger.debug("Starting incremental sync")
|
||||
### 0. Sanity checks
|
||||
if (len(self.mapper)<1):
|
||||
raise ClientFatalError("No source to destination mapping specified")
|
||||
# 0. Sanity checks
|
||||
if (len(self.mapper) < 1):
|
||||
raise ClientFatalError(
|
||||
"No source to destination mapping specified")
|
||||
if (self.mapper.unsafe()):
|
||||
raise ClientFatalError("Source to destination mappings unsafe: %s" % str(self.mapper))
|
||||
raise ClientFatalError(
|
||||
"Source to destination mappings unsafe: %s" % str(
|
||||
self.mapper))
|
||||
from_timestamp = None
|
||||
if (from_datetime is not None):
|
||||
try:
|
||||
from_timestamp = str_to_datetime(from_datetime)
|
||||
except ValueError:
|
||||
raise ClientFatalError("Bad datetime in --from (%s)" % from_datetime)
|
||||
### 1. Work out where to start from
|
||||
raise ClientFatalError(
|
||||
"Bad datetime in --from (%s)" %
|
||||
from_datetime)
|
||||
# 1. Work out where to start from
|
||||
if (from_timestamp is None):
|
||||
from_timestamp=ClientState().get_state(self.sitemap)
|
||||
from_timestamp = ClientState().get_state(self.sitemap)
|
||||
if (from_timestamp is None):
|
||||
raise ClientFatalError("Cannot do incremental sync. No stored timestamp for this site, and no explicit --from.")
|
||||
### 2. Get URI of change list, from sitemap or explicit
|
||||
raise ClientFatalError(
|
||||
"Cannot do incremental sync. No stored timestamp for this site, and no explicit --from.")
|
||||
# 2. Get URI of change list, from sitemap or explicit
|
||||
if (change_list_uri):
|
||||
# Translate as necessary using maps
|
||||
change_list = self.sitemap_uri(change_list_uri)
|
||||
else:
|
||||
# Try default name
|
||||
change_list = self.sitemap_uri(self.change_list_name)
|
||||
### 3. Read change list from source
|
||||
# 3. Read change list from source
|
||||
try:
|
||||
self.logger.info("Reading change list %s" % (change_list))
|
||||
src_change_list = ChangeList()
|
||||
src_change_list.read(uri=change_list)
|
||||
self.logger.debug("Finished reading change list")
|
||||
except Exception as e:
|
||||
raise ClientFatalError("Can't read source change list from %s (%s)" % (change_list,str(e)))
|
||||
self.logger.info("Read source change list, %d changes listed" % (len(src_change_list)))
|
||||
#if (len(src_change_list)==0):
|
||||
raise ClientFatalError(
|
||||
"Can't read source change list from %s (%s)" %
|
||||
(change_list, str(e)))
|
||||
self.logger.info(
|
||||
"Read source change list, %d changes listed" %
|
||||
(len(src_change_list)))
|
||||
# if (len(src_change_list)==0):
|
||||
# raise ClientFatalError("Aborting as there are no resources to sync")
|
||||
if (self.checksum and not src_change_list.has_md5()):
|
||||
self.checksum=False
|
||||
self.logger.info("Not calculating checksums on destination as not present in source change list")
|
||||
self.checksum = False
|
||||
self.logger.info(
|
||||
"Not calculating checksums on destination as not present in source change list")
|
||||
# Check all changes have timestamp and record last
|
||||
self.last_timestamp = 0
|
||||
for resource in src_change_list:
|
||||
if (resource.timestamp is None):
|
||||
raise ClientFatalError("Aborting - missing timestamp for change in %s" % (uri))
|
||||
raise ClientFatalError(
|
||||
"Aborting - missing timestamp for change in %s" %
|
||||
(uri))
|
||||
if (resource.timestamp > self.last_timestamp):
|
||||
self.last_timestamp = resource.timestamp
|
||||
### 4. Check that the change list has authority over URIs listed
|
||||
# 4. Check that the change list has authority over URIs listed
|
||||
# FIXME - What does authority mean for change list? Here use both the
|
||||
# change list URI and, if we used it, the sitemap URI
|
||||
if (not self.noauth):
|
||||
@ -265,15 +300,20 @@ class Client(object):
|
||||
if (not change_list_uri):
|
||||
uauth_sm = UrlAuthority(self.sitemap)
|
||||
for resource in src_change_list:
|
||||
if (not uauth_cs.has_authority_over(resource.uri) and
|
||||
(change_list_uri or not uauth_sm.has_authority_over(resource.uri))):
|
||||
raise ClientFatalError("Aborting as change list (%s) mentions resource at a location it does not have authority over (%s), override with --noauth" % (change_list,resource.uri))
|
||||
### 5. Prune entries before starting timestamp and dupe changes for a resource
|
||||
if (not uauth_cs.has_authority_over(resource.uri) and
|
||||
(change_list_uri or not uauth_sm.has_authority_over(resource.uri))):
|
||||
raise ClientFatalError(
|
||||
"Aborting as change list (%s) mentions resource at a location it does not have authority over (%s), override with --noauth" %
|
||||
(change_list, resource.uri))
|
||||
# 5. Prune entries before starting timestamp and dupe changes for a
|
||||
# resource
|
||||
num_skipped = src_change_list.prune_before(from_timestamp)
|
||||
if (num_skipped>0):
|
||||
self.logger.info("Skipped %d changes before %s" % (num_skipped,datetime_to_str(from_timestamp)))
|
||||
if (num_skipped > 0):
|
||||
self.logger.info(
|
||||
"Skipped %d changes before %s" %
|
||||
(num_skipped, datetime_to_str(from_timestamp)))
|
||||
num_dupes = src_change_list.prune_dupes()
|
||||
if (num_dupes>0):
|
||||
if (num_dupes > 0):
|
||||
self.logger.info("Removed %d prior changes" % (num_dupes))
|
||||
# Review and log status before
|
||||
# FIXME - should at this stage prune the change list to pick out
|
||||
@ -283,25 +323,29 @@ class Client(object):
|
||||
to_delete = 0
|
||||
for resource in src_change_list:
|
||||
if (resource.change == 'updated'):
|
||||
to_update+=1
|
||||
to_update += 1
|
||||
elif (resource.change == 'created'):
|
||||
to_create+=1
|
||||
to_create += 1
|
||||
elif (resource.change == 'deleted'):
|
||||
to_delete+=1
|
||||
to_delete += 1
|
||||
else:
|
||||
raise ClientError("Unknown change type %s" % (resource.change) )
|
||||
raise ClientError("Unknown change type %s" % (resource.change))
|
||||
# Log status based on what we know from the Change List. Exit if
|
||||
# either there are no changes or if there are only deletions and
|
||||
# we don't allow deletion
|
||||
in_sync = ((to_update+to_delete+to_create)==0)
|
||||
self.log_status(in_sync=in_sync, incremental=True, created=to_create,
|
||||
in_sync = ((to_update + to_delete + to_create) == 0)
|
||||
self.log_status(in_sync=in_sync, incremental=True, created=to_create,
|
||||
updated=to_update, deleted=to_delete)
|
||||
if (in_sync or ((to_update+to_create)==0 and not allow_deletion)):
|
||||
if (in_sync or ((to_update + to_create) == 0 and not allow_deletion)):
|
||||
self.logger.debug("Completed incremental")
|
||||
return
|
||||
### 6. Apply changes at same time or after from_timestamp
|
||||
delete_msg = (", and delete %d resources" % to_delete) if (allow_deletion) else ''
|
||||
self.logger.warning("Will apply %d changes%s" % (len(src_change_list),delete_msg))
|
||||
# 6. Apply changes at same time or after from_timestamp
|
||||
delete_msg = (
|
||||
", and delete %d resources" %
|
||||
to_delete) if (allow_deletion) else ''
|
||||
self.logger.warning(
|
||||
"Will apply %d changes%s" %
|
||||
(len(src_change_list), delete_msg))
|
||||
num_updated = 0
|
||||
num_deleted = 0
|
||||
num_created = 0
|
||||
@ -309,25 +353,29 @@ class Client(object):
|
||||
uri = resource.uri
|
||||
filename = self.mapper.src_to_dst(uri)
|
||||
if (resource.change == 'updated'):
|
||||
self.logger.info("updated: %s -> %s" % (uri,filename))
|
||||
self.update_resource(resource,filename,'updated')
|
||||
num_updated+=1
|
||||
self.logger.info("updated: %s -> %s" % (uri, filename))
|
||||
self.update_resource(resource, filename, 'updated')
|
||||
num_updated += 1
|
||||
elif (resource.change == 'created'):
|
||||
self.logger.info("created: %s -> %s" % (uri,filename))
|
||||
self.update_resource(resource,filename,'created')
|
||||
num_created+=1
|
||||
self.logger.info("created: %s -> %s" % (uri, filename))
|
||||
self.update_resource(resource, filename, 'created')
|
||||
num_created += 1
|
||||
elif (resource.change == 'deleted'):
|
||||
num_deleted+=self.delete_resource(resource,filename,allow_deletion)
|
||||
num_deleted += self.delete_resource(
|
||||
resource, filename, allow_deletion)
|
||||
else:
|
||||
raise ClientError("Unknown change type %s" % (resource.change) )
|
||||
### 7. Report status and planned actions
|
||||
self.log_status(incremental=True,created=num_created, updated=num_updated,
|
||||
deleted=num_deleted,to_delete=to_delete)
|
||||
### 8. Record last timestamp we have seen
|
||||
if (self.last_timestamp>0):
|
||||
ClientState().set_state(self.sitemap,self.last_timestamp)
|
||||
self.logger.info("Written last timestamp %s for incremental sync" % (datetime_to_str(self.last_timestamp)))
|
||||
### 9. Done
|
||||
raise ClientError("Unknown change type %s" % (resource.change))
|
||||
# 7. Report status and planned actions
|
||||
self.log_status(incremental=True, created=num_created, updated=num_updated,
|
||||
deleted=num_deleted, to_delete=to_delete)
|
||||
# 8. Record last timestamp we have seen
|
||||
if (self.last_timestamp > 0):
|
||||
ClientState().set_state(self.sitemap, self.last_timestamp)
|
||||
self.logger.info(
|
||||
"Written last timestamp %s for incremental sync" %
|
||||
(datetime_to_str(
|
||||
self.last_timestamp)))
|
||||
# 9. Done
|
||||
self.logger.debug("Completed incremental sync")
|
||||
|
||||
def update_resource(self, resource, filename, change=None):
|
||||
@ -336,8 +384,8 @@ class Client(object):
|
||||
Update means three things:
|
||||
1. GET resources
|
||||
2. set mtime in local time to be equal to timestamp in UTC (should perhaps
|
||||
or at least warn if different from LastModified from the GET response instead
|
||||
but maybe warn if different (or just earlier than) the lastmod we expected
|
||||
or at least warn if different from LastModified from the GET response instead
|
||||
but maybe warn if different (or just earlier than) the lastmod we expected
|
||||
from the resource list
|
||||
3. check that resource matches expected information
|
||||
|
||||
@ -348,16 +396,18 @@ class Client(object):
|
||||
"""
|
||||
path = os.path.dirname(filename)
|
||||
distutils.dir_util.mkpath(path)
|
||||
num_updated=0
|
||||
num_updated = 0
|
||||
if (self.dryrun):
|
||||
self.logger.info("dryrun: would GET %s --> %s" % (resource.uri,filename))
|
||||
self.logger.info(
|
||||
"dryrun: would GET %s --> %s" %
|
||||
(resource.uri, filename))
|
||||
else:
|
||||
# 1. GET
|
||||
try:
|
||||
urlretrieve(resource.uri,filename)
|
||||
num_updated+=1
|
||||
urlretrieve(resource.uri, filename)
|
||||
num_updated += 1
|
||||
except IOError as e:
|
||||
msg = "Failed to GET %s -- %s" % (resource.uri,str(e))
|
||||
msg = "Failed to GET %s -- %s" % (resource.uri, str(e))
|
||||
if (self.ignore_failures):
|
||||
self.logger.warning(msg)
|
||||
return(num_updated)
|
||||
@ -365,62 +415,74 @@ class Client(object):
|
||||
raise ClientFatalError(msg)
|
||||
# 2. set timestamp if we have one
|
||||
if (resource.timestamp is not None):
|
||||
unixtime = int(resource.timestamp) #no fractional
|
||||
os.utime(filename,(unixtime,unixtime))
|
||||
unixtime = int(resource.timestamp) # no fractional
|
||||
os.utime(filename, (unixtime, unixtime))
|
||||
if (resource.timestamp > self.last_timestamp):
|
||||
self.last_timestamp = resource.timestamp
|
||||
self.log_event(Resource(resource=resource, change=change))
|
||||
# 3. sanity check
|
||||
length = os.stat(filename).st_size
|
||||
if (resource.length is not None and resource.length != length):
|
||||
self.logger.info("Downloaded size for %s of %d bytes does not match expected %d bytes" % (resource.uri,length,resource.length))
|
||||
self.logger.info(
|
||||
"Downloaded size for %s of %d bytes does not match expected %d bytes" %
|
||||
(resource.uri, length, resource.length))
|
||||
if (self.checksum and resource.md5 is not None):
|
||||
file_md5 = compute_md5_for_file(filename)
|
||||
if (resource.md5 != file_md5):
|
||||
self.logger.info("MD5 mismatch for %s, got %s but expected %s bytes" % (resource.uri,file_md5,resource.md5))
|
||||
self.logger.info(
|
||||
"MD5 mismatch for %s, got %s but expected %s bytes" %
|
||||
(resource.uri, file_md5, resource.md5))
|
||||
return(num_updated)
|
||||
|
||||
def delete_resource(self, resource, filename, allow_deletion=False):
|
||||
"""Delete copy of resource in filename on local system.
|
||||
|
||||
Will only actually do the deletion if allow_deletion is True. Regardless
|
||||
of whether the deletion occurs, self.last_timestamp will be updated
|
||||
Will only actually do the deletion if allow_deletion is True. Regardless
|
||||
of whether the deletion occurs, self.last_timestamp will be updated
|
||||
if the resource.timestamp is later than the current value.
|
||||
|
||||
Returns the number of files actually deleted (0 or 1).
|
||||
"""
|
||||
num_deleted=0
|
||||
num_deleted = 0
|
||||
uri = resource.uri
|
||||
if (resource.timestamp is not None and
|
||||
resource.timestamp > self.last_timestamp):
|
||||
resource.timestamp > self.last_timestamp):
|
||||
self.last_timestamp = resource.timestamp
|
||||
if (allow_deletion):
|
||||
if (self.dryrun):
|
||||
self.logger.info("dryrun: would delete %s -> %s" % (uri,filename))
|
||||
self.logger.info(
|
||||
"dryrun: would delete %s -> %s" %
|
||||
(uri, filename))
|
||||
else:
|
||||
try:
|
||||
os.unlink(filename)
|
||||
num_deleted+=1
|
||||
self.logger.info("deleted: %s -> %s" % (uri,filename))
|
||||
self.log_event(Resource(resource=resource, change="deleted"))
|
||||
num_deleted += 1
|
||||
self.logger.info("deleted: %s -> %s" % (uri, filename))
|
||||
self.log_event(
|
||||
Resource(
|
||||
resource=resource,
|
||||
change="deleted"))
|
||||
except OSError as e:
|
||||
msg = "Failed to DELETE %s -> %s : %s" % (uri,filename,str(e))
|
||||
#if (self.ignore_failures):
|
||||
msg = "Failed to DELETE %s -> %s : %s" % (
|
||||
uri, filename, str(e))
|
||||
# if (self.ignore_failures):
|
||||
self.logger.warning(msg)
|
||||
# return
|
||||
#else:
|
||||
# else:
|
||||
# raise ClientFatalError(msg)
|
||||
else:
|
||||
self.logger.info("nodelete: would delete %s (--delete to enable)" % uri)
|
||||
self.logger.info(
|
||||
"nodelete: would delete %s (--delete to enable)" %
|
||||
uri)
|
||||
return(num_deleted)
|
||||
|
||||
def parse_document(self):
|
||||
"""Parse any ResourceSync document and show information.
|
||||
|
||||
|
||||
Will use sitemap URI taken either from explicit self.sitemap_name
|
||||
or derived from the mappings supplied.
|
||||
"""
|
||||
s=Sitemap()
|
||||
s = Sitemap()
|
||||
self.logger.info("Reading sitemap(s) from %s ..." % (self.sitemap))
|
||||
try:
|
||||
list = s.parse_xml(url_or_file_open(self.sitemap))
|
||||
@ -430,33 +492,36 @@ class Client(object):
|
||||
capability = '(unknown capability)'
|
||||
if ('capability' in list.md):
|
||||
capability = list.md['capability']
|
||||
print("Parsed %s document with %d entries" % (capability,num_entries))
|
||||
print("Parsed %s document with %d entries" % (capability, num_entries))
|
||||
if (self.verbose):
|
||||
to_show = 100
|
||||
override_str = ' (override with --max-sitemap-entries)'
|
||||
if (self.max_sitemap_entries):
|
||||
to_show = self.max_sitemap_entries
|
||||
override_str = ''
|
||||
if (num_entries>to_show):
|
||||
print("Showing first %d entries sorted by URI%s..." % (to_show,override_str))
|
||||
n=0
|
||||
if (num_entries > to_show):
|
||||
print(
|
||||
"Showing first %d entries sorted by URI%s..." %
|
||||
(to_show, override_str))
|
||||
n = 0
|
||||
for resource in list:
|
||||
print('[%d] %s' % (n,str(resource)))
|
||||
n+=1
|
||||
if ( n >= to_show ):
|
||||
print('[%d] %s' % (n, str(resource)))
|
||||
n += 1
|
||||
if (n >= to_show):
|
||||
break
|
||||
|
||||
def write_resource_list(self,paths=None,outfile=None,links=None,dump=None):
|
||||
def write_resource_list(
|
||||
self, paths=None, outfile=None, links=None, dump=None):
|
||||
"""Write a Resource List or a Resource Dump for files on local disk.
|
||||
|
||||
Set of resources included is based on paths setting or else the mappings.
|
||||
Set of resources included is based on paths setting or else the mappings.
|
||||
Optionally links can be added. Output will be to stdout unless outfile
|
||||
is specified.
|
||||
|
||||
|
||||
If dump is true then a Resource Dump is written instead of a Resource
|
||||
List. If outfile is not set then self.default_resource_dump will be used.
|
||||
"""
|
||||
rl = self.build_resource_list(paths=paths,set_path=dump)
|
||||
rl = self.build_resource_list(paths=paths, set_path=dump)
|
||||
if (links is not None):
|
||||
rl.ln = links
|
||||
if (dump):
|
||||
@ -470,15 +535,17 @@ class Client(object):
|
||||
try:
|
||||
print(rl.as_xml())
|
||||
except ListBaseIndexError as e:
|
||||
raise ClientFatalError("%s. Use --output option to specify base name for output files." % str(e))
|
||||
raise ClientFatalError(
|
||||
"%s. Use --output option to specify base name for output files." %
|
||||
str(e))
|
||||
else:
|
||||
rl.write(basename=outfile)
|
||||
|
||||
def write_change_list(self,paths=None,outfile=None,ref_sitemap=None,newref_sitemap=None,
|
||||
empty=None,links=None,dump=None):
|
||||
def write_change_list(self, paths=None, outfile=None, ref_sitemap=None, newref_sitemap=None,
|
||||
empty=None, links=None, dump=None):
|
||||
"""Write a change list.
|
||||
|
||||
Unless the both ref_sitemap and newref_sitemap are specified then the Change
|
||||
|
||||
Unless the both ref_sitemap and newref_sitemap are specified then the Change
|
||||
List is calculated between the reference an the current state of files on
|
||||
disk. The files on disk are scanned based either on the paths setting or
|
||||
else on the mappings.
|
||||
@ -487,18 +554,19 @@ class Client(object):
|
||||
if (not empty):
|
||||
# 1. Get and parse reference sitemap
|
||||
old_rl = self.read_reference_resource_list(ref_sitemap)
|
||||
# 2. Depending on whether a newref_sitemap was specified, either read that
|
||||
# 2. Depending on whether a newref_sitemap was specified, either read that
|
||||
# or build resource list from files on disk
|
||||
if (newref_sitemap is None):
|
||||
# Get resource list from disk
|
||||
new_rl = self.build_resource_list(paths=paths,set_path=dump)
|
||||
new_rl = self.build_resource_list(paths=paths, set_path=dump)
|
||||
else:
|
||||
new_rl = self.read_reference_resource_list(newref_sitemap,name='new reference')
|
||||
new_rl = self.read_reference_resource_list(
|
||||
newref_sitemap, name='new reference')
|
||||
# 3. Calculate change list
|
||||
(same,updated,deleted,created)=old_rl.compare(new_rl)
|
||||
cl.add_changed_resources( updated, change='updated' )
|
||||
cl.add_changed_resources( deleted, change='deleted' )
|
||||
cl.add_changed_resources( created, change='created' )
|
||||
(same, updated, deleted, created) = old_rl.compare(new_rl)
|
||||
cl.add_changed_resources(updated, change='updated')
|
||||
cl.add_changed_resources(deleted, change='deleted')
|
||||
cl.add_changed_resources(created, change='created')
|
||||
# 4. Write out change list
|
||||
cl.mapper = self.mapper
|
||||
cl.pretty_xml = self.pretty_xml
|
||||
@ -508,9 +576,10 @@ class Client(object):
|
||||
print(cl.as_xml())
|
||||
else:
|
||||
cl.write(basename=outfile)
|
||||
self.write_dump_if_requested(cl,dump)
|
||||
self.write_dump_if_requested(cl, dump)
|
||||
|
||||
def write_capability_list(self,capabilities=None,outfile=None,links=None):
|
||||
def write_capability_list(self, capabilities=None,
|
||||
outfile=None, links=None):
|
||||
"""Write a Capability List to outfile or STDOUT."""
|
||||
capl = CapabilityList(ln=links)
|
||||
capl.pretty_xml = self.pretty_xml
|
||||
@ -522,7 +591,8 @@ class Client(object):
|
||||
else:
|
||||
capl.write(basename=outfile)
|
||||
|
||||
def write_source_description(self,capability_lists=None,outfile=None,links=None):
|
||||
def write_source_description(
|
||||
self, capability_lists=None, outfile=None, links=None):
|
||||
"""Write a ResourceSync Description document to outfile or STDOUT."""
|
||||
rsd = SourceDescription(ln=links)
|
||||
rsd.pretty_xml = self.pretty_xml
|
||||
@ -534,70 +604,76 @@ class Client(object):
|
||||
else:
|
||||
rsd.write(basename=outfile)
|
||||
|
||||
def write_dump_if_requested(self,resource_list,dump):
|
||||
def write_dump_if_requested(self, resource_list, dump):
|
||||
"""Write a dump to the file dump."""
|
||||
if (dump is None):
|
||||
return
|
||||
print("OOPS - FIXME - Wrinting dump to %s not yet implemented" % (dump))
|
||||
return(1)
|
||||
|
||||
def read_reference_resource_list(self,ref_sitemap,name='reference'):
|
||||
def read_reference_resource_list(self, ref_sitemap, name='reference'):
|
||||
"""Read reference resource list and return the ResourceList object.
|
||||
|
||||
The name parameter is used just in output messages to say what type
|
||||
of resource list is being read.
|
||||
"""
|
||||
rl = ResourceList()
|
||||
self.logger.info("Reading %s resource list from %s ..." % (name,ref_sitemap))
|
||||
rl.mapper=self.mapper
|
||||
rl.read(uri=ref_sitemap,index_only=(not self.allow_multifile))
|
||||
self.logger.info(
|
||||
"Reading %s resource list from %s ..." %
|
||||
(name, ref_sitemap))
|
||||
rl.mapper = self.mapper
|
||||
rl.read(uri=ref_sitemap, index_only=(not self.allow_multifile))
|
||||
num_entries = len(rl.resources)
|
||||
self.logger.info("Read %s resource list with %d entries in %d sitemaps" % (name,num_entries,rl.num_files))
|
||||
self.logger.info(
|
||||
"Read %s resource list with %d entries in %d sitemaps" %
|
||||
(name, num_entries, rl.num_files))
|
||||
if (self.verbose):
|
||||
to_show = 100
|
||||
override_str = ' (override with --max-sitemap-entries)'
|
||||
if (self.max_sitemap_entries):
|
||||
to_show = self.max_sitemap_entries
|
||||
override_str = ''
|
||||
if (num_entries>to_show):
|
||||
print("Showing first %d entries sorted by URI%s..." % (to_show,override_str))
|
||||
n=0
|
||||
if (num_entries > to_show):
|
||||
print(
|
||||
"Showing first %d entries sorted by URI%s..." %
|
||||
(to_show, override_str))
|
||||
n = 0
|
||||
for r in rl.resources:
|
||||
print(r)
|
||||
n+=1
|
||||
if ( n >= to_show ):
|
||||
n += 1
|
||||
if (n >= to_show):
|
||||
break
|
||||
return(rl)
|
||||
|
||||
def log_status(self, in_sync=True, incremental=False, audit=False,
|
||||
same=None, created=0, updated=0, deleted=0, to_delete=0):
|
||||
"""Write log message regarding status in standard form.
|
||||
|
||||
|
||||
Split this off so all messages from baseline/audit/incremental
|
||||
are written in a consistent form.
|
||||
"""
|
||||
if (audit):
|
||||
words = { 'created': 'to create',
|
||||
'updated': 'to update',
|
||||
'deleted': 'to delete' }
|
||||
words = {'created': 'to create',
|
||||
'updated': 'to update',
|
||||
'deleted': 'to delete'}
|
||||
else:
|
||||
words = { 'created': 'created',
|
||||
'updated': 'updated',
|
||||
'deleted': 'deleted' }
|
||||
words = {'created': 'created',
|
||||
'updated': 'updated',
|
||||
'deleted': 'deleted'}
|
||||
if in_sync:
|
||||
# status rather than action
|
||||
status = "NO CHANGES" if incremental else "IN SYNC"
|
||||
status = "NO CHANGES" if incremental else "IN SYNC"
|
||||
else:
|
||||
if audit:
|
||||
status = "NOT IN SYNC"
|
||||
elif (to_delete>deleted):
|
||||
#will need --delete
|
||||
elif (to_delete > deleted):
|
||||
# will need --delete
|
||||
status = "PART APPLIED" if incremental else"PART SYNCED"
|
||||
words['deleted']='to delete (--delete)'
|
||||
deleted=to_delete
|
||||
else:
|
||||
words['deleted'] = 'to delete (--delete)'
|
||||
deleted = to_delete
|
||||
else:
|
||||
status = "CHANGES APPLIED" if incremental else "SYNCED"
|
||||
same = "" if (same is None) else ("same=%d, " % same)
|
||||
self.logger.warning("Status: %15s (%s%s=%d, %s=%d, %s=%d)" %\
|
||||
(status, same, words['created'], created,
|
||||
words['updated'], updated, words['deleted'], deleted))
|
||||
same = "" if (same is None) else ("same=%d, " % same)
|
||||
self.logger.warning("Status: %15s (%s%s=%d, %s=%d, %s=%d)" %
|
||||
(status, same, words['created'], created,
|
||||
words['updated'], updated, words['deleted'], deleted))
|
||||
|
||||
@ -8,13 +8,13 @@ of the last change seen.
|
||||
import sys
|
||||
import os.path
|
||||
import datetime
|
||||
import distutils.dir_util
|
||||
import distutils.dir_util
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
try: #python3
|
||||
try: # python3
|
||||
from configparser import ConfigParser, NoSectionError, NoOptionError
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from ConfigParser import SafeConfigParser as ConfigParser, NoSectionError, NoOptionError
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@ class ClientState(object):
|
||||
"""Initialize ClientState object with default status file name."""
|
||||
self.status_file = '.resync-client-status.cfg'
|
||||
|
||||
def set_state(self,site,timestamp=None):
|
||||
def set_state(self, site, timestamp=None):
|
||||
"""Write status dict to client status file.
|
||||
|
||||
|
||||
FIXME - should have some file lock to avoid race
|
||||
"""
|
||||
parser = ConfigParser()
|
||||
@ -36,21 +36,29 @@ class ClientState(object):
|
||||
if (not parser.has_section(status_section)):
|
||||
parser.add_section(status_section)
|
||||
if (timestamp is None):
|
||||
parser.remove_option(status_section, self.config_site_to_name(site))
|
||||
parser.remove_option(
|
||||
status_section,
|
||||
self.config_site_to_name(site))
|
||||
else:
|
||||
parser.set(status_section, self.config_site_to_name(site), str(timestamp))
|
||||
parser.set(
|
||||
status_section,
|
||||
self.config_site_to_name(site),
|
||||
str(timestamp))
|
||||
with open(self.status_file, 'w') as configfile:
|
||||
parser.write(configfile)
|
||||
configfile.close()
|
||||
|
||||
def get_state(self,site):
|
||||
def get_state(self, site):
|
||||
"""Read client status file and return dict."""
|
||||
parser = ConfigParser()
|
||||
status_section = 'incremental'
|
||||
parser.read(self.status_file)
|
||||
timestamp = None
|
||||
try:
|
||||
timestamp = float(parser.get(status_section,self.config_site_to_name(site)))
|
||||
timestamp = float(
|
||||
parser.get(
|
||||
status_section,
|
||||
self.config_site_to_name(site)))
|
||||
except NoSectionError as e:
|
||||
pass
|
||||
except NoOptionError as e:
|
||||
@ -64,4 +72,4 @@ class ClientState(object):
|
||||
lead to multiple site names mapping to one config name but
|
||||
probably not too likely.
|
||||
"""
|
||||
return( re.sub(r"[^\w]",'_',name) )
|
||||
return(re.sub(r"[^\w]", '_', name))
|
||||
|
||||
@ -18,20 +18,22 @@ Copyright 2012-2016 Simeon Warner
|
||||
limitations under the License
|
||||
"""
|
||||
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import urlopen
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import urlopen
|
||||
import logging
|
||||
import logging.config
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
|
||||
class ClientFatalError(Exception):
|
||||
"""Non-recoverable error in client, should include message to user."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UTCFormatter(logging.Formatter):
|
||||
"""Format datetime values as ISO8601 UTC Z form.
|
||||
|
||||
@ -43,8 +45,9 @@ class UTCFormatter(logging.Formatter):
|
||||
timestamp = record.created
|
||||
return datetime.utcfromtimestamp(timestamp).isoformat() + 'Z'
|
||||
|
||||
|
||||
def init_logging(to_file=False, logfile=None, default_logfile='/tmp/resync.log',
|
||||
human=True, verbose=False, eval_mode=False,
|
||||
human=True, verbose=False, eval_mode=False,
|
||||
default_logger='client', extra_loggers=None):
|
||||
"""Initialize logging.
|
||||
|
||||
@ -53,116 +56,126 @@ def init_logging(to_file=False, logfile=None, default_logfile='/tmp/resync.log',
|
||||
INFO - verbose, only seen by users if they ask for it (-v)
|
||||
WARNING - messages output messages to console
|
||||
|
||||
Logging to a file: If to_file is True then output will be written to
|
||||
Logging to a file: If to_file is True then output will be written to
|
||||
a file. This will be logfile if set, else default_logfile (which may
|
||||
also be overridden).
|
||||
"""
|
||||
fmt = '%(asctime)s | %(name)s | %(levelname)s | %(message)s'
|
||||
formatter = UTCFormatter(fmt)
|
||||
|
||||
|
||||
if human:
|
||||
# Create a special handler designed just for human readable output
|
||||
hh = logging.StreamHandler()
|
||||
hh.setLevel( logging.INFO if (verbose) else logging.WARNING )
|
||||
hh.setLevel(logging.INFO if (verbose) else logging.WARNING)
|
||||
hh.setFormatter(logging.Formatter(fmt='%(message)s'))
|
||||
if to_file:
|
||||
if (logfile is None):
|
||||
logfile = default_logfile
|
||||
fh = logging.FileHandler(filename=logfile, mode='a')
|
||||
fh.setFormatter(formatter)
|
||||
fh.setLevel( logging.DEBUG if (eval_mode) else logging.INFO )
|
||||
fh.setLevel(logging.DEBUG if (eval_mode) else logging.INFO)
|
||||
|
||||
loggers = [default_logger,'resync']
|
||||
loggers = [default_logger, 'resync']
|
||||
if (extra_loggers is not None):
|
||||
for logger in extra_loggers:
|
||||
loggers.append(logger)
|
||||
for logger in loggers:
|
||||
log = logging.getLogger(logger)
|
||||
log.setLevel(logging.DEBUG) #control at handler instead
|
||||
log.setLevel(logging.DEBUG) # control at handler instead
|
||||
if human:
|
||||
log.addHandler(hh)
|
||||
if to_file:
|
||||
log.addHandler(fh)
|
||||
|
||||
log=logging.getLogger(default_logger)
|
||||
log = logging.getLogger(default_logger)
|
||||
if (to_file):
|
||||
log.info("Writing detailed log to %s" % (logfile))
|
||||
|
||||
|
||||
def count_true_args(*args):
|
||||
"""Count number of list of arguments that evaluate True."""
|
||||
count=0
|
||||
count = 0
|
||||
for arg in args:
|
||||
if (arg):
|
||||
count+=1
|
||||
count += 1
|
||||
return(count)
|
||||
|
||||
|
||||
def parse_links(args_link):
|
||||
"""Parse --link options.
|
||||
|
||||
Uses parse_link() to parse each option.
|
||||
"""
|
||||
links=[]
|
||||
links = []
|
||||
if (args_link is not None):
|
||||
for link_str in args_link:
|
||||
try:
|
||||
links.append(parse_link(link_str))
|
||||
except ClientFatalError as e:
|
||||
raise ClientFatalError("Bad --link option '%s' (%s)" % (link_str,str(e)))
|
||||
raise ClientFatalError(
|
||||
"Bad --link option '%s' (%s)" %
|
||||
(link_str, str(e)))
|
||||
return(links)
|
||||
|
||||
|
||||
def parse_link(link_str):
|
||||
"""Parse one --link option to add to <rs:ln> links.
|
||||
|
||||
Input string of the form: rel,href,att1=val1,att2=val2
|
||||
"""
|
||||
atts={}
|
||||
help_str = "--link option '%s' (format rel,href,att1=val1...)"%(link_str)
|
||||
atts = {}
|
||||
help_str = "--link option '%s' (format rel,href,att1=val1...)" % (link_str)
|
||||
try:
|
||||
segs = link_str.split(',')
|
||||
# First segments are relation and subject
|
||||
atts['rel'] = segs.pop(0)
|
||||
atts['href'] = segs.pop(0)
|
||||
if (atts['href']==''):
|
||||
if (atts['href'] == ''):
|
||||
raise ClientFatalError("Missing uri in " + help_str)
|
||||
# Remaining segments are attributes
|
||||
for term in segs:
|
||||
(k,v)=term.split('=')
|
||||
if (k=='' or v==''):
|
||||
raise ClientFatalError("Bad attribute (%s) in " % (term) + help_str)
|
||||
atts[k]=v
|
||||
(k, v) = term.split('=')
|
||||
if (k == '' or v == ''):
|
||||
raise ClientFatalError(
|
||||
"Bad attribute (%s) in " %
|
||||
(term) + help_str)
|
||||
atts[k] = v
|
||||
except ValueError as e:
|
||||
raise ClientFatalError("Bad component of " + help_str)
|
||||
except IndexError as e:
|
||||
raise ClientFatalError("Incomplete component of " + help_str)
|
||||
|
||||
|
||||
return(atts)
|
||||
|
||||
|
||||
def parse_capabilities(caps_str):
|
||||
"""Parse list of capabilities in --capabilitylist option.
|
||||
|
||||
Input string of the form: cap_name=uri,cap_name=uri
|
||||
"""
|
||||
capabilities={}
|
||||
capabilities = {}
|
||||
try:
|
||||
segs = caps_str.split(',')
|
||||
for term in segs:
|
||||
(k,v)=term.split('=')
|
||||
capabilities[k]=v
|
||||
(k, v) = term.split('=')
|
||||
capabilities[k] = v
|
||||
except ValueError as e:
|
||||
raise ClientFatalError("Bad component of --capabilitylist option '%s' (%s)"%(caps_str,str(e)))
|
||||
raise ClientFatalError(
|
||||
"Bad component of --capabilitylist option '%s' (%s)" %
|
||||
(caps_str, str(e)))
|
||||
return(capabilities)
|
||||
|
||||
|
||||
def parse_capability_lists(cls_str):
|
||||
"""Parse list of capability lists in --capabilitylistindex option.
|
||||
|
||||
Input string of the form: uri,uri
|
||||
"""
|
||||
return(cls_str.split(','))
|
||||
|
||||
|
||||
|
||||
def url_or_file_open(uri):
|
||||
"""Wrapper around urlopen() to prepend file: if no scheme provided."""
|
||||
if (not re.match(r'''\w+:''',uri)):
|
||||
uri = 'file:'+uri
|
||||
if (not re.match(r'''\w+:''', uri)):
|
||||
uri = 'file:' + uri
|
||||
return(urlopen(uri))
|
||||
|
||||
|
||||
114
resync/dump.py
114
resync/dump.py
@ -5,6 +5,7 @@ import os.path
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from resync.resource_dump_manifest import ResourceDumpManifest
|
||||
|
||||
|
||||
class Dump(object):
|
||||
"""Dump of content for a Resource Dump or Change Dump.
|
||||
|
||||
@ -25,31 +26,33 @@ class Dump(object):
|
||||
self.resources = resources
|
||||
self.format = ('zip' if (format is None) else format)
|
||||
self.compress = compress
|
||||
self.manifest_class = ResourceDumpManifest #FIXME
|
||||
self.max_size = 100*1024*1024 #100MB
|
||||
self.manifest_class = ResourceDumpManifest # FIXME
|
||||
self.max_size = 100 * 1024 * 1024 # 100MB
|
||||
self.max_files = 50000
|
||||
self.path_prefix = None
|
||||
self.logger = logging.getLogger('resync.dump')
|
||||
|
||||
|
||||
def write(self, basename=None, write_separate_manifests=True):
|
||||
"""Write one or more dump files to complete this dump.
|
||||
|
||||
Returns the number of dump/archive files written.
|
||||
"""
|
||||
self.check_files()
|
||||
n=0
|
||||
n = 0
|
||||
for manifest in self.partition_dumps():
|
||||
dumpbase="%s%05d" % (basename,n)
|
||||
dumpfile="%s.%s" % (dumpbase,self.format)
|
||||
dumpbase = "%s%05d" % (basename, n)
|
||||
dumpfile = "%s.%s" % (dumpbase, self.format)
|
||||
if (write_separate_manifests):
|
||||
manifest.write(basename=dumpbase+'.xml')
|
||||
manifest.write(basename=dumpbase + '.xml')
|
||||
if (self.format == 'zip'):
|
||||
self.write_zip(manifest.resources,dumpfile)
|
||||
self.write_zip(manifest.resources, dumpfile)
|
||||
elif (self.format == 'warc'):
|
||||
self.write_warc(manifest.resources,dumpfile)
|
||||
self.write_warc(manifest.resources, dumpfile)
|
||||
else:
|
||||
raise DumpError("Unknown dump format requested (%s)" % (self.format))
|
||||
n+=1
|
||||
raise DumpError(
|
||||
"Unknown dump format requested (%s)" %
|
||||
(self.format))
|
||||
n += 1
|
||||
self.logger.info("Wrote %d dump files" % (n))
|
||||
return(n)
|
||||
|
||||
@ -60,8 +63,12 @@ class Dump(object):
|
||||
a manifest file manifest.xml (written first). No checks on the size of files
|
||||
or total size are performed, this is expected to have been done beforehand.
|
||||
"""
|
||||
compression = ( ZIP_DEFLATED if self.compress else ZIP_STORED )
|
||||
zf = ZipFile(dumpfile, mode="w", compression=compression, allowZip64=True)
|
||||
compression = (ZIP_DEFLATED if self.compress else ZIP_STORED)
|
||||
zf = ZipFile(
|
||||
dumpfile,
|
||||
mode="w",
|
||||
compression=compression,
|
||||
allowZip64=True)
|
||||
# Write resources first
|
||||
rdm = ResourceDumpManifest(resources=resources)
|
||||
real_path = {}
|
||||
@ -69,24 +76,26 @@ class Dump(object):
|
||||
archive_path = self.archive_path(resource.path)
|
||||
real_path[archive_path] = resource.path
|
||||
resource.path = archive_path
|
||||
zf.writestr('manifest.xml',rdm.as_xml())
|
||||
zf.writestr('manifest.xml', rdm.as_xml())
|
||||
# Add all files in the resources
|
||||
for resource in resources:
|
||||
zf.write(real_path[resource.path], arcname=resource.path)
|
||||
zf.close()
|
||||
zipsize = os.path.getsize(dumpfile)
|
||||
self.logger.info("Wrote ZIP file dump %s with size %d bytes" % (dumpfile,zipsize))
|
||||
|
||||
self.logger.info(
|
||||
"Wrote ZIP file dump %s with size %d bytes" %
|
||||
(dumpfile, zipsize))
|
||||
|
||||
def write_warc(self, resources=None, dumpfile=None):
|
||||
"""Write a WARC dump file.
|
||||
|
||||
WARC support is not part of ResourceSync v1.0 (Z39.99 2014) but is left
|
||||
in this library for experimentation.
|
||||
"""
|
||||
# Load library late as we want to be able to run rest of code
|
||||
# Load library late as we want to be able to run rest of code
|
||||
# without this installed
|
||||
try:
|
||||
from warc import WARCFile,WARCHeader,WARCRecord
|
||||
from warc import WARCFile, WARCHeader, WARCRecord
|
||||
except:
|
||||
raise DumpError("Failed to load WARC library")
|
||||
wf = WARCFile(dumpfile, mode="w", compress=self.compress)
|
||||
@ -100,46 +109,56 @@ class Dump(object):
|
||||
wh.result_code = 200
|
||||
wh.checksum = 'aabbcc'
|
||||
wh.location = self.archive_path(resource.path)
|
||||
wf.write_record( WARCRecord( header=wh, payload=resource.path ) )
|
||||
wf.write_record(WARCRecord(header=wh, payload=resource.path))
|
||||
wf.close()
|
||||
warcsize = os.path.getsize(dumpfile)
|
||||
self.logging.info("Wrote WARC file dump %s with size %d bytes" % (dumpfile,warcsize))
|
||||
|
||||
def check_files(self,set_length=True,check_length=True):
|
||||
self.logging.info(
|
||||
"Wrote WARC file dump %s with size %d bytes" %
|
||||
(dumpfile, warcsize))
|
||||
|
||||
def check_files(self, set_length=True, check_length=True):
|
||||
"""Check all files in self.resources, find longest common prefix.
|
||||
|
||||
Go though and check all files in self.resources, add up size, and find
|
||||
longest common path that can be used when writing the dump file. Saved in
|
||||
longest common path that can be used when writing the dump file. Saved in
|
||||
self.path_prefix.
|
||||
|
||||
Parameters set_length and check_length control control whether then set_length
|
||||
attribute should be set from the file size if not specified, and whether any
|
||||
attribute should be set from the file size if not specified, and whether any
|
||||
length specified should be checked. By default both are True. In any event, the
|
||||
total size calculated is the size of files on disk.
|
||||
"""
|
||||
total_size = 0 #total size of all files in bytes
|
||||
total_size = 0 # total size of all files in bytes
|
||||
path_prefix = None
|
||||
for resource in self.resources:
|
||||
if (resource.path is None):
|
||||
#explicit test because exception raised by getsize otherwise confusing
|
||||
raise DumpError("No file path defined for resource %s" % resource.uri)
|
||||
# explicit test because exception raised by getsize otherwise
|
||||
# confusing
|
||||
raise DumpError(
|
||||
"No file path defined for resource %s" %
|
||||
resource.uri)
|
||||
if (path_prefix is None):
|
||||
path_prefix = os.path.dirname(resource.path)
|
||||
else:
|
||||
path_prefix = os.path.commonprefix( [ path_prefix, os.path.dirname(resource.path) ])
|
||||
path_prefix = os.path.commonprefix(
|
||||
[path_prefix, os.path.dirname(resource.path)])
|
||||
size = os.path.getsize(resource.path)
|
||||
if (resource.length is not None):
|
||||
if (check_length and resource.length!=size):
|
||||
if (check_length and resource.length != size):
|
||||
raise DumpError("Size of resource %s is %d on disk, not %d as specified" %
|
||||
(resource.uri, size, resource.length) )
|
||||
(resource.uri, size, resource.length))
|
||||
elif (set_length):
|
||||
resource.length = size
|
||||
if (size > self.max_size):
|
||||
raise DumpError("Size of file (%s, %d) exceeds maximum (%d) dump size" % (resource.path,size,self.max_size))
|
||||
raise DumpError(
|
||||
"Size of file (%s, %d) exceeds maximum (%d) dump size" %
|
||||
(resource.path, size, self.max_size))
|
||||
total_size += size
|
||||
self.path_prefix = path_prefix
|
||||
self.total_size = total_size
|
||||
self.logger.info("Total size of files to include in dump %d bytes" % (total_size))
|
||||
self.logger.info(
|
||||
"Total size of files to include in dump %d bytes" %
|
||||
(total_size))
|
||||
return True
|
||||
|
||||
def partition_dumps(self):
|
||||
@ -147,27 +166,26 @@ class Dump(object):
|
||||
|
||||
Simply adds resources/files to a manifest until their are either the
|
||||
the correct number of files or the size limit is exceeded, then yields
|
||||
that manifest.
|
||||
that manifest.
|
||||
"""
|
||||
manifest=self.manifest_class()
|
||||
manifest_size=0
|
||||
manifest_files=0
|
||||
manifest = self.manifest_class()
|
||||
manifest_size = 0
|
||||
manifest_files = 0
|
||||
for resource in self.resources:
|
||||
manifest.add(resource)
|
||||
manifest_size+=resource.length
|
||||
manifest_files+=1
|
||||
if (manifest_size>=self.max_size or
|
||||
manifest_files>=self.max_files):
|
||||
manifest_size += resource.length
|
||||
manifest_files += 1
|
||||
if (manifest_size >= self.max_size or
|
||||
manifest_files >= self.max_files):
|
||||
yield(manifest)
|
||||
# Need to start a new manifest
|
||||
manifest=self.manifest_class()
|
||||
manifest_size=0
|
||||
manifest_files=0
|
||||
if (manifest_files>0):
|
||||
manifest = self.manifest_class()
|
||||
manifest_size = 0
|
||||
manifest_files = 0
|
||||
if (manifest_files > 0):
|
||||
yield(manifest)
|
||||
|
||||
|
||||
def archive_path(self,real_path):
|
||||
def archive_path(self, real_path):
|
||||
"""Return the archive path for file with real_path.
|
||||
|
||||
Mapping is based on removal of self.path_prefix which is determined
|
||||
@ -176,10 +194,10 @@ class Dump(object):
|
||||
if (not self.path_prefix):
|
||||
return(real_path)
|
||||
else:
|
||||
return( os.path.relpath(real_path,self.path_prefix) )
|
||||
return(os.path.relpath(real_path, self.path_prefix))
|
||||
|
||||
|
||||
class DumpError(Exception):
|
||||
"""Error class used by Dump() objects."""
|
||||
|
||||
|
||||
pass
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
"""ResourceSync explorer.
|
||||
|
||||
This is the guts of a client designed to 'explore' the ResourceSync
|
||||
facilities offered by a source. Will use standard practices to
|
||||
facilities offered by a source. Will use standard practices to
|
||||
look for and interpret capabilities.
|
||||
"""
|
||||
|
||||
import sys
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.parse import urlparse, urlunparse, urljoin
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urlparse import urlparse, urlunparse, urljoin
|
||||
input = raw_input
|
||||
import os.path
|
||||
import datetime
|
||||
import distutils.dir_util
|
||||
import distutils.dir_util
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
@ -21,23 +21,24 @@ import requests
|
||||
|
||||
from .mapper import Mapper
|
||||
from .sitemap import Sitemap
|
||||
from .client import Client,ClientFatalError
|
||||
from .client import Client, ClientFatalError
|
||||
from .client_state import ClientState
|
||||
from .client_utils import ClientFatalError, url_or_file_open
|
||||
from .resource import Resource
|
||||
from .w3c_datetime import str_to_datetime,datetime_to_str
|
||||
from .w3c_datetime import str_to_datetime, datetime_to_str
|
||||
|
||||
|
||||
class Explorer(Client):
|
||||
"""Extension of the client code to explore a ResourceSync source.
|
||||
|
||||
Designed to support a text-based command-line client that starts from
|
||||
a given URI, server, or local file, and then allows interactive
|
||||
a given URI, server, or local file, and then allows interactive
|
||||
exploration of ResourceSync capabilities offered by the source.
|
||||
"""
|
||||
|
||||
def explore(self):
|
||||
"""INTERACTIVE exploration source capabilities.
|
||||
|
||||
|
||||
Will use sitemap URI taken either from explicit self.sitemap_name
|
||||
or derived from the mappings supplied.
|
||||
"""
|
||||
@ -47,57 +48,67 @@ class Explorer(Client):
|
||||
starts = []
|
||||
if (self.sitemap_name is not None):
|
||||
print("Starting from explicit --sitemap %s" % (self.sitemap_name))
|
||||
starts.append( XResource(self.sitemap_name) )
|
||||
elif (len(self.mapper)>0):
|
||||
starts.append(XResource(self.sitemap_name))
|
||||
elif (len(self.mapper) > 0):
|
||||
uri = self.mapper.default_src_uri()
|
||||
(scheme, netloc, path, params, query, fragment) = urlparse(uri)
|
||||
if (not scheme and not netloc):
|
||||
if (os.path.isdir(path)):
|
||||
# have a dir, look for 'likely' file names
|
||||
print("Looking for capability documents in local directory %s" % (path))
|
||||
for name in ['resourcesync','capabilities.xml',
|
||||
'resourcelist.xml','changelist.xml']:
|
||||
file = os.path.join(path,name)
|
||||
print(
|
||||
"Looking for capability documents in local directory %s" %
|
||||
(path))
|
||||
for name in ['resourcesync', 'capabilities.xml',
|
||||
'resourcelist.xml', 'changelist.xml']:
|
||||
file = os.path.join(path, name)
|
||||
if (os.path.isfile(file)):
|
||||
starts.append( XResource(file) )
|
||||
if (len(starts)==0):
|
||||
raise ClientFatalError( "No likely capability files found in local directory %s" %
|
||||
(path) )
|
||||
starts.append(XResource(file))
|
||||
if (len(starts) == 0):
|
||||
raise ClientFatalError("No likely capability files found in local directory %s" %
|
||||
(path))
|
||||
else:
|
||||
# local file, might be anything (or not exist)
|
||||
print("Starting from local file %s" % (path))
|
||||
starts.append( XResource(path) )
|
||||
starts.append(XResource(path))
|
||||
else:
|
||||
# remote, can't tell whether we have a sitemap or a server name or something
|
||||
# remote, can't tell whether we have a sitemap or a server name or something
|
||||
# else, build list of options depending on whether there is a path and whether
|
||||
# there is an extension/name
|
||||
well_known = urlunparse( [ scheme,netloc,'/.well-known/resourcesync','','','' ] )
|
||||
well_known = urlunparse(
|
||||
[scheme, netloc, '/.well-known/resourcesync', '', '', ''])
|
||||
if (not path):
|
||||
# root, just look for .well-known
|
||||
starts.append( XResource(well_known, ['capabilitylist','capabilitylistindex']) )
|
||||
starts.append(
|
||||
XResource(
|
||||
well_known, [
|
||||
'capabilitylist', 'capabilitylistindex']))
|
||||
else:
|
||||
starts.append( XResource(uri) )
|
||||
starts.append( XResource(well_known, ['capabilitylist','capabilitylistindex']) )
|
||||
starts.append(XResource(uri))
|
||||
starts.append(
|
||||
XResource(
|
||||
well_known, [
|
||||
'capabilitylist', 'capabilitylistindex']))
|
||||
print("Looking for discovery information based on mappings")
|
||||
else:
|
||||
raise ClientFatalError("No source information (server base uri or capability uri) specified, use -h for help")
|
||||
#
|
||||
raise ClientFatalError(
|
||||
"No source information (server base uri or capability uri) specified, use -h for help")
|
||||
#
|
||||
# Have list of one or more possible starting point, try them in turn
|
||||
try:
|
||||
for start in starts:
|
||||
# For each starting point we create a fresh history
|
||||
history = [ start ]
|
||||
history = [start]
|
||||
input = None
|
||||
while (len(history)>0):
|
||||
while (len(history) > 0):
|
||||
print()
|
||||
xr = history.pop()
|
||||
new_xr = self.explore_uri(xr,len(history)>0)
|
||||
new_xr = self.explore_uri(xr, len(history) > 0)
|
||||
if (new_xr):
|
||||
# Add current and new to history
|
||||
history.append( xr )
|
||||
history.append( new_xr )
|
||||
history.append(xr)
|
||||
history.append(new_xr)
|
||||
except ExplorerQuit:
|
||||
pass # expected way to exit
|
||||
pass # expected way to exit
|
||||
print("\nresync-explorer done, bye...\n")
|
||||
|
||||
def explore_uri(self, explorer_resource, show_back=True):
|
||||
@ -109,31 +120,32 @@ class Explorer(Client):
|
||||
caps = explorer_resource.acceptable_capabilities
|
||||
checks = explorer_resource.checks
|
||||
print("Reading %s" % (uri))
|
||||
options={}
|
||||
capability=None
|
||||
options = {}
|
||||
capability = None
|
||||
try:
|
||||
if (caps=='resource'):
|
||||
if (caps == 'resource'):
|
||||
# Not expecting a capability document
|
||||
self.explore_show_head(uri,check_headers=checks)
|
||||
else:
|
||||
s=Sitemap()
|
||||
self.explore_show_head(uri, check_headers=checks)
|
||||
else:
|
||||
s = Sitemap()
|
||||
list = s.parse_xml(url_or_file_open(uri))
|
||||
(options,capability)=self.explore_show_summary(list,s.parsed_index,caps,context=uri)
|
||||
(options, capability) = self.explore_show_summary(
|
||||
list, s.parsed_index, caps, context=uri)
|
||||
except IOError as e:
|
||||
print("Cannot read %s (%s)" % (uri,str(e)))
|
||||
print("Cannot read %s (%s)" % (uri, str(e)))
|
||||
except Exception as e:
|
||||
print("Cannot parse %s (%s)" % (uri,str(e)))
|
||||
print("Cannot parse %s (%s)" % (uri, str(e)))
|
||||
#
|
||||
# Loop until we have some valid input
|
||||
#
|
||||
while (True):
|
||||
# don't offer number option for no resources/capabilities
|
||||
num_prompt = '' if (len(options)==0) else 'number, '
|
||||
num_prompt = '' if (len(options) == 0) else 'number, '
|
||||
up_prompt = 'b(ack), ' if (show_back) else ''
|
||||
if (self.fake_input):
|
||||
inp = self.fake_input
|
||||
else:
|
||||
inp = input( "Follow [%s%sq(uit)]?" % (num_prompt,up_prompt) )
|
||||
inp = input("Follow [%s%sq(uit)]?" % (num_prompt, up_prompt))
|
||||
if (inp in options.keys()):
|
||||
break
|
||||
if (inp == 'q'):
|
||||
@ -145,9 +157,9 @@ class Explorer(Client):
|
||||
#
|
||||
checks = {}
|
||||
r = options[inp]
|
||||
if ( r.capability is None ):
|
||||
if (capability in ['resourcelist','changelist',
|
||||
'resourcedump','changedump']):
|
||||
if (r.capability is None):
|
||||
if (capability in ['resourcelist', 'changelist',
|
||||
'resourcedump', 'changedump']):
|
||||
caps = 'resource'
|
||||
else:
|
||||
caps = self.allowed_entries(capability)
|
||||
@ -157,21 +169,22 @@ class Explorer(Client):
|
||||
caps = [r.capability]
|
||||
# Record anything we know about the resource to check
|
||||
if (r.length is not None):
|
||||
checks['content-length']=r.length
|
||||
checks['content-length'] = r.length
|
||||
if (r.lastmod is not None):
|
||||
checks['last-modified']=r.lastmod
|
||||
checks['last-modified'] = r.lastmod
|
||||
if (r.mime_type is not None):
|
||||
checks['content-type']=r.mime_type
|
||||
checks['content-type'] = r.mime_type
|
||||
# FIXME - could add fixity checks here too
|
||||
return( XResource(r.uri, caps, checks) )
|
||||
return(XResource(r.uri, caps, checks))
|
||||
|
||||
def explore_show_summary(self, list, index=False, expected=None, context=None):
|
||||
def explore_show_summary(self, list, index=False,
|
||||
expected=None, context=None):
|
||||
"""Show summary of one capability document.
|
||||
|
||||
Given a capability document or index (in list, index True if it is an
|
||||
index), write out a simply textual summary of the document with all
|
||||
related documents shown as numbered options (of the form
|
||||
[#] ...description...) which will then form a menu for the next
|
||||
Given a capability document or index (in list, index True if it is an
|
||||
index), write out a simply textual summary of the document with all
|
||||
related documents shown as numbered options (of the form
|
||||
[#] ...description...) which will then form a menu for the next
|
||||
exploration.
|
||||
|
||||
If expected is not None then it should be a list of expected document
|
||||
@ -186,75 +199,79 @@ class Explorer(Client):
|
||||
capability = list.md['capability']
|
||||
if (index):
|
||||
capability += 'index'
|
||||
print("Parsed %s document with %d entries:" % (capability,num_entries))
|
||||
print(
|
||||
"Parsed %s document with %d entries:" %
|
||||
(capability, num_entries))
|
||||
if (expected is not None and capability not in expected):
|
||||
print("WARNING - expected a %s document" % (','.join(expected)))
|
||||
if (capability not in ['description','descriptionoindex','capabilitylist',
|
||||
'resourcelist','resourcelistindex','changelist','changelistindex',
|
||||
'resourcedump','resourcedumpindex','changedump','changedumpindex',
|
||||
if (capability not in ['description', 'descriptionoindex', 'capabilitylist',
|
||||
'resourcelist', 'resourcelistindex', 'changelist', 'changelistindex',
|
||||
'resourcedump', 'resourcedumpindex', 'changedump', 'changedumpindex',
|
||||
'resourcelist-archive', 'resourcedump-archive',
|
||||
'changelist-archive', 'changedump-archive']):
|
||||
print("WARNING - unknown %s document type" % (capability))
|
||||
to_show = num_entries
|
||||
if (num_entries>21):
|
||||
if (num_entries > 21):
|
||||
to_show = 20
|
||||
# What capability entries are allowed/expected?
|
||||
entry_caps = self.allowed_entries(capability);
|
||||
# What capability entries are allowed/expected?
|
||||
entry_caps = self.allowed_entries(capability)
|
||||
options = {}
|
||||
n=0
|
||||
n = 0
|
||||
# Look for <rs:ln> elements in this document
|
||||
ln_describedby = list.link('describedby')
|
||||
if (ln_describedby):
|
||||
if ('href' in ln_describedby):
|
||||
uri = ln_describedby['href']
|
||||
print("[%s] rel='describedby' link to %s" % ('d',uri))
|
||||
uri = self.expand_relative_uri(context,uri)
|
||||
options['d']=Resource(uri,capability='resource')
|
||||
print("[%s] rel='describedby' link to %s" % ('d', uri))
|
||||
uri = self.expand_relative_uri(context, uri)
|
||||
options['d'] = Resource(uri, capability='resource')
|
||||
else:
|
||||
print("WARNING - describedby link with no href, ignoring")
|
||||
ln_up = list.link('up')
|
||||
if (ln_up):
|
||||
if ('href' in ln_up):
|
||||
uri = ln_up['href']
|
||||
print("[%s] rel='up' link to %s" % ('u',uri))
|
||||
uri = self.expand_relative_uri(context,uri)
|
||||
options['u']=Resource(uri)
|
||||
print("[%s] rel='up' link to %s" % ('u', uri))
|
||||
uri = self.expand_relative_uri(context, uri)
|
||||
options['u'] = Resource(uri)
|
||||
else:
|
||||
print("WARNING - up link with no href, ignoring")
|
||||
ln_index = list.link('index')
|
||||
if (ln_index):
|
||||
if ('href' in ln_index):
|
||||
uri = ln_index['href']
|
||||
print("[%s] rel='index' link to %s" % ('i',uri))
|
||||
uri = self.expand_relative_uri(context,uri)
|
||||
options['i']=Resource(uri)
|
||||
print("[%s] rel='index' link to %s" % ('i', uri))
|
||||
uri = self.expand_relative_uri(context, uri)
|
||||
options['i'] = Resource(uri)
|
||||
else:
|
||||
print("WARNING - index link with no href, ignoring")
|
||||
# Show listed resources as numbered options
|
||||
for r in list.resources:
|
||||
if (n>=to_show):
|
||||
print("(not showing remaining %d entries)" % (num_entries-n))
|
||||
if (n >= to_show):
|
||||
print("(not showing remaining %d entries)" % (num_entries - n))
|
||||
break
|
||||
n+=1
|
||||
options[str(n)]=r
|
||||
print("[%d] %s" % (n,r.uri))
|
||||
n += 1
|
||||
options[str(n)] = r
|
||||
print("[%d] %s" % (n, r.uri))
|
||||
if (self.verbose):
|
||||
print(" " + str(r))
|
||||
r.uri = self.expand_relative_uri(context,r.uri)
|
||||
r.uri = self.expand_relative_uri(context, r.uri)
|
||||
if (r.capability is not None):
|
||||
warning = ''
|
||||
if (r.capability not in entry_caps):
|
||||
warning = " (EXPECTED %s)" % (' or '.join(entry_caps))
|
||||
print(" %s%s" % (r.capability,warning))
|
||||
elif (len(entry_caps)==1):
|
||||
r.capability=entry_caps[0]
|
||||
print(" capability not specified, should be %s" % (r.capability))
|
||||
return(options,capability)
|
||||
print(" %s%s" % (r.capability, warning))
|
||||
elif (len(entry_caps) == 1):
|
||||
r.capability = entry_caps[0]
|
||||
print(
|
||||
" capability not specified, should be %s" %
|
||||
(r.capability))
|
||||
return(options, capability)
|
||||
|
||||
def explore_show_head(self,uri,check_headers=None):
|
||||
def explore_show_head(self, uri, check_headers=None):
|
||||
"""Do HEAD on uri and show infomation.
|
||||
|
||||
Will also check headers against any values specified in
|
||||
Will also check headers against any values specified in
|
||||
check_headers.
|
||||
"""
|
||||
print("HEAD %s" % (uri))
|
||||
@ -265,44 +282,49 @@ class Explorer(Client):
|
||||
# Mock up response if we have a local file
|
||||
response = self.head_on_file(uri)
|
||||
print(" status: %s" % (response.status_code))
|
||||
if (response.status_code=='200'):
|
||||
if (response.status_code == '200'):
|
||||
# print some of the headers
|
||||
for header in ['content-length','last-modified','lastmod','content-type','etag']:
|
||||
for header in ['content-length', 'last-modified',
|
||||
'lastmod', 'content-type', 'etag']:
|
||||
if header in response.headers:
|
||||
check_str=''
|
||||
check_str = ''
|
||||
if (check_headers is not None and
|
||||
header in check_headers):
|
||||
header in check_headers):
|
||||
if (response.headers[header] == check_headers[header]):
|
||||
check_str=' MATCHES EXPECTED VALUE'
|
||||
check_str = ' MATCHES EXPECTED VALUE'
|
||||
else:
|
||||
check_str=' EXPECTED %s' % (check_headers[header])
|
||||
print(" %s: %s%s" % (header, response.headers[header], check_str))
|
||||
check_str = ' EXPECTED %s' % (
|
||||
check_headers[header])
|
||||
print(
|
||||
" %s: %s%s" %
|
||||
(header, response.headers[header], check_str))
|
||||
|
||||
def head_on_file(self,file):
|
||||
def head_on_file(self, file):
|
||||
"""Mock up requests.head(..) response on local file."""
|
||||
response = HeadResponse()
|
||||
if (not os.path.isfile(file)):
|
||||
response.status_code='404'
|
||||
response.status_code = '404'
|
||||
else:
|
||||
response.status_code='200'
|
||||
response.headers['last-modified']=datetime_to_str(os.path.getmtime(file))
|
||||
response.headers['content-length']=os.path.getsize(file)
|
||||
response.status_code = '200'
|
||||
response.headers[
|
||||
'last-modified'] = datetime_to_str(os.path.getmtime(file))
|
||||
response.headers['content-length'] = os.path.getsize(file)
|
||||
return(response)
|
||||
|
||||
def allowed_entries(self,capability):
|
||||
def allowed_entries(self, capability):
|
||||
"""Return list of allowed entries for given capability document.
|
||||
|
||||
Includes handling of capability = *index where the only acceptable
|
||||
Includes handling of capability = *index where the only acceptable
|
||||
entries are *.
|
||||
"""
|
||||
index = re.match(r'(.+)index$',capability)
|
||||
archive = re.match(r'(.+)\-archive$',capability)
|
||||
index = re.match(r'(.+)index$', capability)
|
||||
archive = re.match(r'(.+)\-archive$', capability)
|
||||
if (capability == 'capabilitylistindex'):
|
||||
return([]) #not allowed so no valid references
|
||||
return([]) # not allowed so no valid references
|
||||
elif (index):
|
||||
return([index.group(1)]) #name without index ending
|
||||
return([index.group(1)]) # name without index ending
|
||||
elif (archive):
|
||||
return([archive.group(1)]) #name without -archive ending
|
||||
return([archive.group(1)]) # name without -archive ending
|
||||
elif (capability == 'description'):
|
||||
return(['capabilitylist'])
|
||||
elif (capability == 'capabilitylist'):
|
||||
@ -312,12 +334,12 @@ class Explorer(Client):
|
||||
'changelist-archive', 'changedump-archive'])
|
||||
return([])
|
||||
|
||||
def expand_relative_uri(self,context,uri):
|
||||
def expand_relative_uri(self, context, uri):
|
||||
"""If uri is relative then expand in context.
|
||||
|
||||
|
||||
Prints warning if expansion happens.
|
||||
"""
|
||||
full_uri = urljoin(context,uri)
|
||||
full_uri = urljoin(context, uri)
|
||||
if (full_uri != uri):
|
||||
print(" WARNING - expanded relative URI to %s" % (full_uri))
|
||||
uri = full_uri
|
||||
@ -328,20 +350,21 @@ class XResource(object):
|
||||
"""Information about a resource for the explorer.
|
||||
|
||||
Must have a uri but may also store:
|
||||
|
||||
acceptable_capabilities - None for any acceptable, 'resource' if a
|
||||
resource rather than a capability document is expected, else
|
||||
|
||||
acceptable_capabilities - None for any acceptable, 'resource' if a
|
||||
resource rather than a capability document is expected, else
|
||||
a list of capability names
|
||||
checks - a set of information to check when then XResource is inspected
|
||||
base_uri - on creation interpret any relative URI specified in the
|
||||
base_uri - on creation interpret any relative URI specified in the
|
||||
context of base_uri, store the resulting full URI
|
||||
"""
|
||||
|
||||
def __init__(self, uri, acceptable_capabilities=None, checks=None, context=None):
|
||||
def __init__(self, uri, acceptable_capabilities=None,
|
||||
checks=None, context=None):
|
||||
"""Initialize XResource object."""
|
||||
self.uri=urljoin(context,uri)
|
||||
self.acceptable_capabilities=acceptable_capabilities
|
||||
self.checks=checks
|
||||
self.uri = urljoin(context, uri)
|
||||
self.acceptable_capabilities = acceptable_capabilities
|
||||
self.checks = checks
|
||||
|
||||
|
||||
class HeadResponse(object):
|
||||
@ -349,8 +372,8 @@ class HeadResponse(object):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize with no status_code and no headers."""
|
||||
self.status_code=None
|
||||
self.headers={}
|
||||
self.status_code = None
|
||||
self.headers = {}
|
||||
|
||||
|
||||
class ExplorerQuit(Exception):
|
||||
|
||||
@ -10,38 +10,39 @@ import os
|
||||
from datetime import datetime
|
||||
import re
|
||||
import sys
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
import logging
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import URLopener
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import URLopener
|
||||
|
||||
from .resource_container import ResourceContainer
|
||||
from .sitemap import Sitemap
|
||||
|
||||
|
||||
class ListBase(ResourceContainer):
|
||||
"""Class that adds Sitemap based IO to ResourceContainer.
|
||||
|
||||
resources - an iterable of resources
|
||||
|
||||
count - add optional explicit setting of the number of items in
|
||||
count - add optional explicit setting of the number of items in
|
||||
resources which is useful when this is an iterator/generator.
|
||||
Is used instead of trying len(resources)
|
||||
|
||||
md - metadata information for the list (<rs:md>)
|
||||
|
||||
|
||||
ln - link information for the list (<rs:ln>)
|
||||
|
||||
sitemapindex - defaults to False, set True if this is an index object
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, count=None, md=None, ln=None, uri=None,
|
||||
def __init__(self, resources=None, count=None, md=None, ln=None, uri=None,
|
||||
capability_name='unknown'):
|
||||
"""Initialize ListBase."""
|
||||
super(ListBase, self).__init__(resources=resources, md=md, ln=ln, uri=uri,
|
||||
@ -73,9 +74,9 @@ class ListBase(ResourceContainer):
|
||||
return(self.count)
|
||||
return(len(self.resources))
|
||||
|
||||
##### INPUT #####
|
||||
# INPUT
|
||||
|
||||
def read(self,uri=None):
|
||||
def read(self, uri=None):
|
||||
"""Default case is just to parse document at this URI.
|
||||
|
||||
Intention is that the read() method may be overridden to support reading
|
||||
@ -83,10 +84,10 @@ class ListBase(ResourceContainer):
|
||||
"""
|
||||
self.parse(uri=uri)
|
||||
|
||||
def parse(self,uri=None,fh=None,str_data=None,**kwargs):
|
||||
def parse(self, uri=None, fh=None, str_data=None, **kwargs):
|
||||
"""Parse a single XML document for this list.
|
||||
|
||||
Accepts either a uri (uri or default if parameter not specified),
|
||||
Accepts either a uri (uri or default if parameter not specified),
|
||||
or a filehandle (fh) or a string (str_data). Note that this method
|
||||
does not handle the case of a sitemapindex+sitemaps.
|
||||
|
||||
@ -97,45 +98,52 @@ class ListBase(ResourceContainer):
|
||||
try:
|
||||
fh = URLopener().open(uri)
|
||||
except IOError as e:
|
||||
raise Exception("Failed to load sitemap/sitemapindex from %s (%s)" % (uri,str(e)))
|
||||
raise Exception(
|
||||
"Failed to load sitemap/sitemapindex from %s (%s)" %
|
||||
(uri, str(e)))
|
||||
elif (str_data is not None):
|
||||
fh=io.StringIO(str_data)
|
||||
fh = io.StringIO(str_data)
|
||||
elif ('str' in kwargs):
|
||||
# Legacy support for str argument, see
|
||||
# https://github.com/resync/resync/pull/21
|
||||
# One test for this in tests/test_list_base.py
|
||||
self.logger.warn("Legacy parse(str=...), use parse(str_data=...) instead")
|
||||
self.logger.warn(
|
||||
"Legacy parse(str=...), use parse(str_data=...) instead")
|
||||
fh = io.StringIO(kwargs['str'])
|
||||
if (fh is None):
|
||||
raise Exception("Nothing to parse")
|
||||
s = self.new_sitemap()
|
||||
s.parse_xml(fh=fh,resources=self,capability=self.capability_name,sitemapindex=False)
|
||||
s.parse_xml(
|
||||
fh=fh,
|
||||
resources=self,
|
||||
capability=self.capability_name,
|
||||
sitemapindex=False)
|
||||
self.parsed_index = s.parsed_index
|
||||
|
||||
##### OUTPUT #####
|
||||
|
||||
# OUTPUT
|
||||
|
||||
def as_xml(self):
|
||||
"""Return XML serialization of this list.
|
||||
|
||||
This code does not support the case where the list is too big for
|
||||
This code does not support the case where the list is too big for
|
||||
a single XML document.
|
||||
"""
|
||||
self.default_capability()
|
||||
s = self.new_sitemap()
|
||||
return s.resources_as_xml(self,sitemapindex=self.sitemapindex)
|
||||
return s.resources_as_xml(self, sitemapindex=self.sitemapindex)
|
||||
|
||||
def write(self,basename="/tmp/resynclist.xml"):
|
||||
def write(self, basename="/tmp/resynclist.xml"):
|
||||
"""Write a single sitemap or sitemapindex XML document.
|
||||
|
||||
Must be overridden to support multi-file lists.
|
||||
"""
|
||||
self.default_capability()
|
||||
fh = open(basename,'w')
|
||||
fh = open(basename, 'w')
|
||||
s = self.new_sitemap()
|
||||
s.resources_as_xml(self,fh=fh,sitemapindex=self.sitemapindex)
|
||||
s.resources_as_xml(self, fh=fh, sitemapindex=self.sitemapindex)
|
||||
fh.close()
|
||||
|
||||
##### UTILITY #####
|
||||
# UTILITY
|
||||
|
||||
def new_sitemap(self):
|
||||
"""Create new Sitemap object with default settings."""
|
||||
|
||||
@ -5,15 +5,15 @@ sitemapindexes. Extends ListBase to add the support for sitemapindexes.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import math
|
||||
import math
|
||||
import os
|
||||
from datetime import datetime
|
||||
import re
|
||||
import sys
|
||||
import itertools
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import URLopener
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import URLopener
|
||||
|
||||
from .list_base import ListBase
|
||||
@ -23,6 +23,7 @@ from .mapper import Mapper, MapperError
|
||||
from .url_authority import UrlAuthority
|
||||
from .utils import compute_md5_for_file
|
||||
|
||||
|
||||
class ListBaseWithIndex(ListBase):
|
||||
"""Class that add handling of sitemapindexes to ListBase.
|
||||
|
||||
@ -49,25 +50,27 @@ class ListBaseWithIndex(ListBase):
|
||||
to those that the component sitemap files will be exposed as
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, count=None, md=None, ln=None, uri=None,
|
||||
def __init__(self, resources=None, count=None, md=None, ln=None, uri=None,
|
||||
capability_name='unknown', allow_multifile=None, mapper=None,
|
||||
resources_class=None):
|
||||
"""Initialize ListBaseWithIndex."""
|
||||
self.resources_class = list if resources_class is None else resources_class
|
||||
if (resources is None):
|
||||
resources = self.resources_class()
|
||||
super(ListBaseWithIndex, self).__init__(resources=resources, count=count, md=md, ln=ln,
|
||||
super(ListBaseWithIndex, self).__init__(resources=resources, count=count, md=md, ln=ln,
|
||||
uri=uri, capability_name=capability_name)
|
||||
# specific to lists with indexes
|
||||
self.max_sitemap_entries=50000
|
||||
self.max_sitemap_entries = 50000
|
||||
self.mapper = mapper
|
||||
self.allow_multifile = (True if (allow_multifile is None) else allow_multifile)
|
||||
self.allow_multifile = (
|
||||
True if (
|
||||
allow_multifile is None) else allow_multifile)
|
||||
self.check_url_authority = False
|
||||
self.content_length = 0
|
||||
self.num_files = 0 # Number of files read
|
||||
self.bytes_read = 0 # Aggregate of content_length values
|
||||
|
||||
##### INPUT #####
|
||||
# INPUT
|
||||
|
||||
def read(self, uri=None, resources=None, index_only=False):
|
||||
"""Read sitemap from a URI including handling sitemapindexes.
|
||||
@ -76,7 +79,7 @@ class ListBaseWithIndex(ListBase):
|
||||
will not be read. This will result in no resources being returned and is
|
||||
useful only to read the metadata and links listed in the sitemapindex.
|
||||
|
||||
Includes the subtlety that if the input URI is a local file and is a
|
||||
Includes the subtlety that if the input URI is a local file and is a
|
||||
sitemapindex which contains URIs for the individual sitemaps, then these
|
||||
are mapped to the filesystem also.
|
||||
"""
|
||||
@ -84,63 +87,81 @@ class ListBaseWithIndex(ListBase):
|
||||
fh = URLopener().open(uri)
|
||||
self.num_files += 1
|
||||
except IOError as e:
|
||||
raise IOError("Failed to load sitemap/sitemapindex from %s (%s)" % (uri,str(e)))
|
||||
raise IOError(
|
||||
"Failed to load sitemap/sitemapindex from %s (%s)" %
|
||||
(uri, str(e)))
|
||||
# Get the Content-Length if we can (works fine for local files)
|
||||
try:
|
||||
self.content_length = int(fh.info()['Content-Length'])
|
||||
self.bytes_read += self.content_length
|
||||
self.logger.debug( "Read %d bytes from %s" % (self.content_length,uri) )
|
||||
self.logger.debug(
|
||||
"Read %d bytes from %s" %
|
||||
(self.content_length, uri))
|
||||
except KeyError:
|
||||
# If we don't get a length then c'est la vie
|
||||
self.logger.debug( "Read ????? bytes from %s" % (uri) )
|
||||
self.logger.debug("Read ????? bytes from %s" % (uri))
|
||||
pass
|
||||
self.logger.info( "Read sitemap/sitemapindex from %s" % (uri) )
|
||||
self.logger.info("Read sitemap/sitemapindex from %s" % (uri))
|
||||
s = self.new_sitemap()
|
||||
s.parse_xml(fh=fh,resources=self,capability=self.capability_name)
|
||||
s.parse_xml(fh=fh, resources=self, capability=self.capability_name)
|
||||
# what did we read? sitemap or sitemapindex?
|
||||
if (s.parsed_index):
|
||||
# sitemapindex
|
||||
if (not self.allow_multifile):
|
||||
raise ListBaseIndexError("Got sitemapindex from %s but support for sitemapindex disabled" % (uri))
|
||||
self.logger.info( "Parsed as sitemapindex, %d sitemaps" % (len(self.resources)) )
|
||||
raise ListBaseIndexError(
|
||||
"Got sitemapindex from %s but support for sitemapindex disabled" %
|
||||
(uri))
|
||||
self.logger.info(
|
||||
"Parsed as sitemapindex, %d sitemaps" %
|
||||
(len(
|
||||
self.resources)))
|
||||
sitemapindex_is_file = self.is_file_uri(uri)
|
||||
if (index_only):
|
||||
# don't read the component sitemaps
|
||||
self.sitemapindex = True
|
||||
return
|
||||
# now loop over all entries to read each sitemap and add to resources
|
||||
# now loop over all entries to read each sitemap and add to
|
||||
# resources
|
||||
sitemaps = self.resources
|
||||
self.resources = self.resources_class()
|
||||
self.logger.info( "Now reading %d sitemaps" % len(sitemaps.uris()) )
|
||||
self.logger.info("Now reading %d sitemaps" % len(sitemaps.uris()))
|
||||
for sitemap_uri in sorted(sitemaps.uris()):
|
||||
self.read_component_sitemap(uri,sitemap_uri,s,sitemapindex_is_file)
|
||||
self.read_component_sitemap(
|
||||
uri, sitemap_uri, s, sitemapindex_is_file)
|
||||
else:
|
||||
# sitemap
|
||||
self.logger.info( "Parsed as sitemap, %d resources" % (len(self.resources)) )
|
||||
self.logger.info("Parsed as sitemap, %d resources" %
|
||||
(len(self.resources)))
|
||||
|
||||
|
||||
def read_component_sitemap(self, sitemapindex_uri, sitemap_uri, sitemap, sitemapindex_is_file):
|
||||
def read_component_sitemap(
|
||||
self, sitemapindex_uri, sitemap_uri, sitemap, sitemapindex_is_file):
|
||||
"""Read a component sitemap of a Resource List with index.
|
||||
|
||||
Each component must be a sitemap with the
|
||||
Each component must be a sitemap with the
|
||||
"""
|
||||
if (sitemapindex_is_file):
|
||||
if (not self.is_file_uri(sitemap_uri)):
|
||||
# Attempt to map URI to local file
|
||||
remote_uri = sitemap_uri
|
||||
sitemap_uri = self.mapper.src_to_dst(remote_uri)
|
||||
self.logger.info("Mapped %s to local file %s" % (remote_uri, sitemap_uri))
|
||||
self.logger.info(
|
||||
"Mapped %s to local file %s" %
|
||||
(remote_uri, sitemap_uri))
|
||||
else:
|
||||
# The individual sitemaps should be at a URL (scheme/server/path)
|
||||
# that the sitemapindex URL can speak authoritatively about
|
||||
if (self.check_url_authority and
|
||||
not UrlAuthority(sitemapindex_uri).has_authority_over(sitemap_uri)):
|
||||
raise ListBaseIndexError("The sitemapindex (%s) refers to sitemap at a location it does not have authority over (%s)" % (sitemapindex_uri,sitemap_uri))
|
||||
not UrlAuthority(sitemapindex_uri).has_authority_over(sitemap_uri)):
|
||||
raise ListBaseIndexError(
|
||||
"The sitemapindex (%s) refers to sitemap at a location it does not have authority over (%s)" %
|
||||
(sitemapindex_uri, sitemap_uri))
|
||||
try:
|
||||
fh = URLopener().open(sitemap_uri)
|
||||
self.num_files += 1
|
||||
except IOError as e:
|
||||
raise ListBaseIndexError("Failed to load sitemap from %s listed in sitemap index %s (%s)" % (sitemap_uri,sitemapindex_uri,str(e)))
|
||||
raise ListBaseIndexError(
|
||||
"Failed to load sitemap from %s listed in sitemap index %s (%s)" %
|
||||
(sitemap_uri, sitemapindex_uri, str(e)))
|
||||
# Get the Content-Length if we can (works fine for local files)
|
||||
try:
|
||||
self.content_length = int(fh.info()['Content-Length'])
|
||||
@ -148,34 +169,36 @@ class ListBaseWithIndex(ListBase):
|
||||
except KeyError:
|
||||
# If we don't get a length then c'est la vie
|
||||
pass
|
||||
self.logger.info( "Reading sitemap from %s (%d bytes)" % (sitemap_uri,self.content_length) )
|
||||
component = sitemap.parse_xml( fh=fh, sitemapindex=False )
|
||||
self.logger.info(
|
||||
"Reading sitemap from %s (%d bytes)" %
|
||||
(sitemap_uri, self.content_length))
|
||||
component = sitemap.parse_xml(fh=fh, sitemapindex=False)
|
||||
# Copy resources into self, check any metadata
|
||||
for r in component:
|
||||
self.resources.add(r)
|
||||
# FIXME - if rel="up" check it goes to correct place
|
||||
# FIXME - check capability
|
||||
|
||||
##### OUTPUT #####
|
||||
# OUTPUT
|
||||
|
||||
def requires_multifile(self):
|
||||
"""Return False or the number of component sitemaps required.
|
||||
|
||||
|
||||
In the case that no len() is available for self.resources then
|
||||
then self.count must be set beforehand to avoid an exception.
|
||||
"""
|
||||
if (self.max_sitemap_entries is None or
|
||||
len(self)<=self.max_sitemap_entries):
|
||||
len(self) <= self.max_sitemap_entries):
|
||||
return(False)
|
||||
return( int( math.ceil( len(self) / float(self.max_sitemap_entries) ) ) )
|
||||
return(int(math.ceil(len(self) / float(self.max_sitemap_entries))))
|
||||
|
||||
def as_xml(self, allow_multifile=False, basename="/tmp/sitemap.xml"):
|
||||
"""Return XML serialization of this list.
|
||||
|
||||
If this list can be serialized as a single sitemap then the
|
||||
If this list can be serialized as a single sitemap then the
|
||||
superclass method is used.
|
||||
|
||||
If there is no single XML serialization (in the case that the
|
||||
If there is no single XML serialization (in the case that the
|
||||
number of list resources is more than is allowed in a single sitemap)
|
||||
then raise an exception unless allow_multifile is set True.
|
||||
If allow_multifile is set True then will return the sitemapindex
|
||||
@ -186,55 +209,65 @@ class ListBaseWithIndex(ListBase):
|
||||
elif (allow_multifile):
|
||||
return self.as_xml_index(basename)
|
||||
else:
|
||||
raise ListBaseIndexError("Attempt to write single XML string for list with %d entries when max_sitemap_entries is set to %d" % (len(self),self.max_sitemap_entries))
|
||||
raise ListBaseIndexError(
|
||||
"Attempt to write single XML string for list with %d entries when max_sitemap_entries is set to %d" %
|
||||
(len(self), self.max_sitemap_entries))
|
||||
|
||||
def as_xml_index(self, basename="/tmp/sitemap.xml"):
|
||||
"""Return a string of the index for a large list that is split.
|
||||
|
||||
|
||||
All we need to do is determine the number of component sitemaps will
|
||||
be is and generate their URIs based on a pattern.
|
||||
|
||||
Q - should there be a flag to select generation of each component sitemap
|
||||
in order to calculate the md5sum?
|
||||
|
||||
|
||||
Q - what timestamp should be used?
|
||||
"""
|
||||
num_parts = self.requires_multifile()
|
||||
if (not num_parts):
|
||||
raise ListBaseIndexError("Request for sitemapindex for list with only %d entries when max_sitemap_entries is set to %s" % (len(self),str(self.max_sitemap_entries)))
|
||||
index=ListBase()
|
||||
index.sitemapindex=True
|
||||
raise ListBaseIndexError(
|
||||
"Request for sitemapindex for list with only %d entries when max_sitemap_entries is set to %s" %
|
||||
(len(self), str(
|
||||
self.max_sitemap_entries)))
|
||||
index = ListBase()
|
||||
index.sitemapindex = True
|
||||
index.capability_name = self.capability_name
|
||||
index.default_capability()
|
||||
for n in range(num_parts):
|
||||
r = Resource( uri = self.part_name(basename,n) )
|
||||
r = Resource(uri=self.part_name(basename, n))
|
||||
index.add(r)
|
||||
return( index.as_xml() )
|
||||
return(index.as_xml())
|
||||
|
||||
def as_xml_part(self, basename="/tmp/sitemap.xml", part_number=0):
|
||||
"""Return a string of component sitemap number part_number.
|
||||
|
||||
Used in the case of a large list that is split into component
|
||||
sitemaps.
|
||||
|
||||
|
||||
basename is used to create "index" links to the sitemapindex
|
||||
|
||||
|
||||
Q - what timestamp should be used?
|
||||
"""
|
||||
if (not self.requires_multifile()):
|
||||
raise ListBaseIndexError("Request for component sitemap for list with only %d entries when max_sitemap_entries is set to %s" % (len(self),str(self.max_sitemap_entries)))
|
||||
raise ListBaseIndexError(
|
||||
"Request for component sitemap for list with only %d entries when max_sitemap_entries is set to %s" %
|
||||
(len(self), str(
|
||||
self.max_sitemap_entries)))
|
||||
start = part_number * self.max_sitemap_entries
|
||||
if (start>len(self)):
|
||||
raise ListBaseIndexError("Request for component sitemap with part_number too high, would start at entry %d yet the list has only %d entries" % (start,len(self)))
|
||||
if (start > len(self)):
|
||||
raise ListBaseIndexError(
|
||||
"Request for component sitemap with part_number too high, would start at entry %d yet the list has only %d entries" %
|
||||
(start, len(self)))
|
||||
stop = start + self.max_sitemap_entries
|
||||
if (stop>len(self)):
|
||||
stop=len(self)
|
||||
part = ListBase( itertools.islice(self.resources,start,stop) )
|
||||
if (stop > len(self)):
|
||||
stop = len(self)
|
||||
part = ListBase(itertools.islice(self.resources, start, stop))
|
||||
part.capability_name = self.capability_name
|
||||
part.default_capability()
|
||||
part.index = basename
|
||||
s = self.new_sitemap()
|
||||
return( s.resources_as_xml(part) )
|
||||
return(s.resources_as_xml(part))
|
||||
|
||||
def write(self, basename='/tmp/sitemap.xml'):
|
||||
"""Write one or a set of sitemap files to disk.
|
||||
@ -243,43 +276,47 @@ class ListBaseWithIndex(ListBase):
|
||||
a ChangeList. This may be a generator so data is read as needed
|
||||
and length is determined at the end.
|
||||
|
||||
basename is used as the name of the single sitemap file or the
|
||||
basename is used as the name of the single sitemap file or the
|
||||
sitemapindex for a set of sitemap files.
|
||||
|
||||
Uses self.max_sitemap_entries to determine whether the resource_list can
|
||||
be written as one sitemap. If there are more entries and
|
||||
self.allow_multifile is set True then a set of sitemap files,
|
||||
Uses self.max_sitemap_entries to determine whether the resource_list can
|
||||
be written as one sitemap. If there are more entries and
|
||||
self.allow_multifile is set True then a set of sitemap files,
|
||||
with an sitemapindex, will be written.
|
||||
"""
|
||||
# Access resources through iterator only
|
||||
resources_iter = iter(self.resources)
|
||||
( chunk, nxt ) = self.get_resources_chunk(resources_iter)
|
||||
(chunk, nxt) = self.get_resources_chunk(resources_iter)
|
||||
s = self.new_sitemap()
|
||||
if (nxt is not None):
|
||||
# Have more than self.max_sitemap_entries => sitemapindex
|
||||
if (not self.allow_multifile):
|
||||
raise ListBaseIndexError("Too many entries for a single sitemap but multifile disabled")
|
||||
raise ListBaseIndexError(
|
||||
"Too many entries for a single sitemap but multifile disabled")
|
||||
# Work out URI of sitemapindex so that we can link up to
|
||||
# it from the individual sitemap files
|
||||
try:
|
||||
index_uri = self.mapper.dst_to_src(basename)
|
||||
except MapperError as e:
|
||||
raise ListBaseIndexError("Cannot map sitemapindex filename to URI (%s)" % str(e))
|
||||
raise ListBaseIndexError(
|
||||
"Cannot map sitemapindex filename to URI (%s)" %
|
||||
str(e))
|
||||
# Use iterator over all resources and count off sets of
|
||||
# max_sitemap_entries to go into each sitemap, store the
|
||||
# names of the sitemaps as we go. Copy md from self into
|
||||
# the index and use this for all chunks also
|
||||
index=ListBase(md=self.md.copy(), ln=list(self.ln))
|
||||
index = ListBase(md=self.md.copy(), ln=list(self.ln))
|
||||
index.capability_name = self.capability_name
|
||||
index.default_capability()
|
||||
while (len(chunk)>0):
|
||||
file = self.part_name(basename,len(index))
|
||||
while (len(chunk) > 0):
|
||||
file = self.part_name(basename, len(index))
|
||||
# Check that we can map the filename of this sitemap into
|
||||
# URI space for the sitemapindex
|
||||
try:
|
||||
uri = self.mapper.dst_to_src(file)
|
||||
except MapperError as e:
|
||||
raise ListBaseIndexError("Cannot map sitemap filename to URI (%s)" % str(e))
|
||||
raise ListBaseIndexError(
|
||||
"Cannot map sitemap filename to URI (%s)" % str(e))
|
||||
self.logger.info("Writing sitemap %s..." % (file))
|
||||
f = open(file, 'w')
|
||||
chunk.index = index_uri
|
||||
@ -287,16 +324,16 @@ class ListBaseWithIndex(ListBase):
|
||||
s.resources_as_xml(chunk, fh=f)
|
||||
f.close()
|
||||
# Record information about this sitemap for index
|
||||
r = Resource( uri = uri,
|
||||
timestamp = os.stat(file).st_mtime,
|
||||
md5 = compute_md5_for_file(file) )
|
||||
r = Resource(uri=uri,
|
||||
timestamp=os.stat(file).st_mtime,
|
||||
md5=compute_md5_for_file(file))
|
||||
index.add(r)
|
||||
# Get next chunk
|
||||
( chunk, nxt ) = self.get_resources_chunk(resources_iter,nxt)
|
||||
(chunk, nxt) = self.get_resources_chunk(resources_iter, nxt)
|
||||
self.logger.info("Wrote %d sitemaps" % (len(index)))
|
||||
f = open(basename, 'w')
|
||||
self.logger.info("Writing sitemapindex %s..." % (basename))
|
||||
s.resources_as_xml(index,sitemapindex=True,fh=f)
|
||||
s.resources_as_xml(index, sitemapindex=True, fh=f)
|
||||
f.close()
|
||||
self.logger.info("Wrote sitemapindex %s" % (basename))
|
||||
else:
|
||||
@ -310,62 +347,63 @@ class ListBaseWithIndex(ListBase):
|
||||
"""XML serialization of this list taken to be sitemapindex entries."""
|
||||
self.default_capability()
|
||||
s = self.new_sitemap()
|
||||
return s.resources_as_xml(self,sitemapindex=True)
|
||||
return s.resources_as_xml(self, sitemapindex=True)
|
||||
|
||||
##### Utility #####
|
||||
# Utility
|
||||
|
||||
def get_resources_chunk(self, resource_iter, first=None):
|
||||
"""Return next chunk of resources from resource_iter, and next item.
|
||||
|
||||
|
||||
If first parameter is specified then this will be prepended to
|
||||
the list.
|
||||
|
||||
The chunk will contain self.max_sitemap_entries if the iterator
|
||||
The chunk will contain self.max_sitemap_entries if the iterator
|
||||
returns that many. next will have the value of the next value from
|
||||
the iterator, providing indication of whether more is available.
|
||||
the iterator, providing indication of whether more is available.
|
||||
Use this as first when asking for the following chunk.
|
||||
"""
|
||||
chunk = ListBase( md=self.md.copy(), ln=list(self.ln) )
|
||||
chunk = ListBase(md=self.md.copy(), ln=list(self.ln))
|
||||
chunk.capability_name = self.capability_name
|
||||
chunk.default_capability()
|
||||
if (first is not None):
|
||||
chunk.add(first)
|
||||
for r in resource_iter:
|
||||
chunk.add(r)
|
||||
if (len(chunk)>=self.max_sitemap_entries):
|
||||
if (len(chunk) >= self.max_sitemap_entries):
|
||||
break
|
||||
# Get next to see whether there are more resources
|
||||
try:
|
||||
nxt = next(resource_iter)
|
||||
except StopIteration:
|
||||
nxt = None
|
||||
return(chunk,nxt)
|
||||
return(chunk, nxt)
|
||||
|
||||
def part_name(self, basename='/tmp/sitemap.xml', part_number=0):
|
||||
"""Name (file or URI) for one component sitemap.
|
||||
|
||||
|
||||
Works for both filenames and URIs because manipulates only the end
|
||||
of the string.
|
||||
|
||||
|
||||
Abstracting this into a function that starts from the basename to get
|
||||
prefix and suffix each time seems a bit wasteful but perhaps not worth
|
||||
worrying about. Allows same code to be used for the write() and
|
||||
worrying about. Allows same code to be used for the write() and
|
||||
as_xml_index() cases.
|
||||
"""
|
||||
# Work out how to name the sitemaps, attempt to add %05d before ".xml$", else append
|
||||
# Work out how to name the sitemaps, attempt to add %05d before
|
||||
# ".xml$", else append
|
||||
sitemap_prefix = basename
|
||||
sitemap_suffix = '.xml'
|
||||
if (basename[-4:] == '.xml'):
|
||||
sitemap_prefix = basename[:-4]
|
||||
return( sitemap_prefix + ( "%05d" % (part_number) ) + sitemap_suffix )
|
||||
|
||||
return(sitemap_prefix + ("%05d" % (part_number)) + sitemap_suffix)
|
||||
|
||||
def is_file_uri(self, uri):
|
||||
"""Return true if uri looks like a local file URI, false otherwise.
|
||||
|
||||
|
||||
Test is to see whether have either an explicit file: URI or whether
|
||||
there is no scheme name.
|
||||
"""
|
||||
return(re.match('file:',uri) or not re.match('\w{3,4}:',uri))
|
||||
return(re.match('file:', uri) or not re.match('\w{3,4}:', uri))
|
||||
|
||||
|
||||
class ListBaseIndexError(Exception):
|
||||
|
||||
@ -250,11 +250,11 @@ class Resource(object):
|
||||
"""
|
||||
hashvals = []
|
||||
if (self.md5 is not None):
|
||||
hashvals.append('md5:'+self.md5)
|
||||
hashvals.append('md5:' + self.md5)
|
||||
if (self.sha1 is not None):
|
||||
hashvals.append('sha-1:'+self.sha1)
|
||||
hashvals.append('sha-1:' + self.sha1)
|
||||
if (self.sha256 is not None):
|
||||
hashvals.append('sha-256:'+self.sha256)
|
||||
hashvals.append('sha-256:' + self.sha256)
|
||||
if (len(hashvals) > 0):
|
||||
return(' '.join(hashvals))
|
||||
return(None)
|
||||
@ -424,7 +424,7 @@ class Resource(object):
|
||||
# not equal if only one timestamp specified
|
||||
if (self.timestamp is None or
|
||||
other.timestamp is None or
|
||||
abs(self.timestamp-other.timestamp) >= delta):
|
||||
abs(self.timestamp - other.timestamp) >= delta):
|
||||
return(False)
|
||||
if ((self.md5 is not None and other.md5 is not None) and
|
||||
self.md5 != other.md5):
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
All documents in ResourceSync have the same types of information:
|
||||
they have some top-level metadata and links, and then a list of
|
||||
resources, each of which may also have metadata and links. This
|
||||
resources, each of which may also have metadata and links. This
|
||||
class implements this model.
|
||||
|
||||
This is a base class for the ListBase class which is in turn the
|
||||
@ -13,6 +13,7 @@ adds IO.
|
||||
import collections
|
||||
from .w3c_datetime import datetime_to_str
|
||||
|
||||
|
||||
class ResourceContainer(object):
|
||||
"""Class containing resource-like objects.
|
||||
|
||||
@ -30,13 +31,14 @@ class ResourceContainer(object):
|
||||
should use only the core functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, md=None, ln=None, uri=None, capability_name=None):
|
||||
def __init__(self, resources=None, md=None, ln=None,
|
||||
uri=None, capability_name=None):
|
||||
"""Initialize ResourceContainer."""
|
||||
self.resources=(resources if (resources is not None) else [])
|
||||
self.md=(md if (md is not None) else {})
|
||||
self.ln=(ln if (ln is not None) else [])
|
||||
self.uri=uri
|
||||
self.capability_name=capability_name
|
||||
self.resources = (resources if (resources is not None) else [])
|
||||
self.md = (md if (md is not None) else {})
|
||||
self.ln = (ln if (ln is not None) else [])
|
||||
self.uri = uri
|
||||
self.capability_name = capability_name
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterator over all the resources in this resource list.
|
||||
@ -45,7 +47,7 @@ class ResourceContainer(object):
|
||||
"""
|
||||
return(iter(self.resources))
|
||||
|
||||
def __getitem__(self,index):
|
||||
def __getitem__(self, index):
|
||||
"""Feed through for __getitem__ of resources property."""
|
||||
return(self.resources[index])
|
||||
|
||||
@ -58,8 +60,8 @@ class ResourceContainer(object):
|
||||
return(None)
|
||||
|
||||
@capability.setter
|
||||
def capability(self,capability):
|
||||
self.md['capability']=capability
|
||||
def capability(self, capability):
|
||||
self.md['capability'] = capability
|
||||
|
||||
@property
|
||||
def md_from(self):
|
||||
@ -70,8 +72,8 @@ class ResourceContainer(object):
|
||||
return(None)
|
||||
|
||||
@md_from.setter
|
||||
def md_from(self,md_from):
|
||||
self.md['md_from']=self._str_datetime_now(md_from)
|
||||
def md_from(self, md_from):
|
||||
self.md['md_from'] = self._str_datetime_now(md_from)
|
||||
|
||||
@property
|
||||
def md_until(self):
|
||||
@ -82,8 +84,8 @@ class ResourceContainer(object):
|
||||
return(None)
|
||||
|
||||
@md_until.setter
|
||||
def md_until(self,md_until):
|
||||
self.md['md_until']=self._str_datetime_now(md_until)
|
||||
def md_until(self, md_until):
|
||||
self.md['md_until'] = self._str_datetime_now(md_until)
|
||||
|
||||
@property
|
||||
def md_at(self):
|
||||
@ -94,8 +96,8 @@ class ResourceContainer(object):
|
||||
return(None)
|
||||
|
||||
@md_at.setter
|
||||
def md_at(self,md_at):
|
||||
self.md['md_at']=self._str_datetime_now(md_at)
|
||||
def md_at(self, md_at):
|
||||
self.md['md_at'] = self._str_datetime_now(md_at)
|
||||
|
||||
@property
|
||||
def md_completed(self):
|
||||
@ -106,29 +108,29 @@ class ResourceContainer(object):
|
||||
return(None)
|
||||
|
||||
@md_completed.setter
|
||||
def md_completed(self,md_completed):
|
||||
self.md['md_completed']=self._str_datetime_now(md_completed)
|
||||
def md_completed(self, md_completed):
|
||||
self.md['md_completed'] = self._str_datetime_now(md_completed)
|
||||
|
||||
def link(self,rel):
|
||||
def link(self, rel):
|
||||
"""Look for link with specified rel, return else None."""
|
||||
for link in self.ln:
|
||||
if ('rel' in link and
|
||||
link['rel']==rel):
|
||||
link['rel'] == rel):
|
||||
return(link)
|
||||
return(None)
|
||||
|
||||
def link_href(self,rel):
|
||||
def link_href(self, rel):
|
||||
"""Look for link with specified rel, return href from it or None."""
|
||||
link = self.link(rel)
|
||||
if (link is not None):
|
||||
link = link['href']
|
||||
return(link)
|
||||
|
||||
def link_set(self,rel,href,**atts):
|
||||
def link_set(self, rel, href, **atts):
|
||||
"""Set/create link with specified rel, set href and any other attributes.
|
||||
|
||||
Any link element must have both rel and href values, the specification
|
||||
also defines the type attributes and others are permitted also. See
|
||||
also defines the type attributes and others are permitted also. See
|
||||
description of allowed formats in
|
||||
|
||||
http://www.openarchives.org/rs/resourcesync.html#DocumentFormats
|
||||
@ -139,7 +141,7 @@ class ResourceContainer(object):
|
||||
link['href'] = href
|
||||
else:
|
||||
# create new link
|
||||
link = {'rel':rel,'href':href}
|
||||
link = {'rel': rel, 'href': href}
|
||||
self.ln.append(link)
|
||||
for k in atts:
|
||||
link[k] = atts[k]
|
||||
@ -150,9 +152,9 @@ class ResourceContainer(object):
|
||||
return(self.link_href('describedby'))
|
||||
|
||||
@describedby.setter
|
||||
def describedby(self,uri):
|
||||
def describedby(self, uri):
|
||||
"""Set ResourceSync Description link to given URI."""
|
||||
self.link_set('describedby',uri)
|
||||
self.link_set('describedby', uri)
|
||||
|
||||
@property
|
||||
def up(self):
|
||||
@ -160,9 +162,9 @@ class ResourceContainer(object):
|
||||
return(self.link_href('up'))
|
||||
|
||||
@up.setter
|
||||
def up(self,uri):
|
||||
def up(self, uri):
|
||||
"""Set ResourceSync rel="up" link to given URI."""
|
||||
self.link_set('up',uri)
|
||||
self.link_set('up', uri)
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
@ -170,9 +172,9 @@ class ResourceContainer(object):
|
||||
return(self.link_href('index'))
|
||||
|
||||
@index.setter
|
||||
def index(self,uri):
|
||||
def index(self, uri):
|
||||
"""Set index link to given URI."""
|
||||
self.link_set('index',uri)
|
||||
self.link_set('index', uri)
|
||||
|
||||
def default_capability(self):
|
||||
"""Set capability name in md.
|
||||
@ -181,7 +183,7 @@ class ResourceContainer(object):
|
||||
capability attributes.
|
||||
"""
|
||||
if ('capability' not in self.md and self.capability_name is not None):
|
||||
self.md['capability']=self.capability_name
|
||||
self.md['capability'] = self.capability_name
|
||||
|
||||
def add(self, resource):
|
||||
"""Add a resource or an iterable collection of resources to this container.
|
||||
@ -203,7 +205,7 @@ class ResourceContainer(object):
|
||||
|
||||
def prune_before(self, timestamp):
|
||||
"""Remove all resources with timestamp earlier than that given.
|
||||
|
||||
|
||||
Returns the number of entries removed. Will raise an excpetion
|
||||
if there are any entries without a timestamp.
|
||||
"""
|
||||
@ -221,8 +223,8 @@ class ResourceContainer(object):
|
||||
|
||||
def prune_dupes(self):
|
||||
"""Remove all but the last entry for a given resource URI.
|
||||
|
||||
Returns the number of entries removed. Also removes all entries for a
|
||||
|
||||
Returns the number of entries removed. Also removes all entries for a
|
||||
given URI where the first entry is a create and the last entry is a
|
||||
delete.
|
||||
"""
|
||||
@ -234,12 +236,12 @@ class ResourceContainer(object):
|
||||
if (r.uri in seen):
|
||||
n += 1
|
||||
if (r.uri in deletes):
|
||||
deletes[r.uri]=r.change
|
||||
deletes[r.uri] = r.change
|
||||
else:
|
||||
pruned1.append(r)
|
||||
seen.add(r.uri)
|
||||
if (r.change == 'deleted'):
|
||||
deletes[r.uri]=r.change
|
||||
deletes[r.uri] = r.change
|
||||
# go through all deletes and prune if first was create
|
||||
pruned2 = []
|
||||
for r in reversed(pruned1):
|
||||
@ -247,7 +249,7 @@ class ResourceContainer(object):
|
||||
n += 1
|
||||
else:
|
||||
pruned2.append(r)
|
||||
self.resources=pruned2
|
||||
self.resources = pruned2
|
||||
return(n)
|
||||
|
||||
def __str__(self):
|
||||
@ -259,7 +261,7 @@ class ResourceContainer(object):
|
||||
|
||||
def _str_datetime_now(self, x=None):
|
||||
"""Return datetime string for use with time attributes.
|
||||
|
||||
|
||||
Handling depends on input:
|
||||
'now' - returns datetime for now
|
||||
number - assume datetime values, generate string
|
||||
@ -267,7 +269,7 @@ class ResourceContainer(object):
|
||||
"""
|
||||
if (x == 'now'):
|
||||
# Now, this is wht datetime_to_str() with no arg gives
|
||||
return( datetime_to_str() )
|
||||
return(datetime_to_str())
|
||||
try:
|
||||
# Test for number
|
||||
junk = x + 0.0
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""ResourceSync ResourceDumpManifest object.
|
||||
|
||||
A ResourceDumpManifest lists the set of files/resources included
|
||||
A ResourceDumpManifest lists the set of files/resources included
|
||||
within a content package that is included in a ResourceDump.
|
||||
|
||||
The ResourceDumpManifest object may also contain metadata and links.
|
||||
@ -11,6 +11,7 @@ http://www.openarchives.org/rs/resourcesync#ResourceDumpManifest
|
||||
|
||||
from .resource_list import ResourceList
|
||||
|
||||
|
||||
class ResourceDumpManifest(ResourceList):
|
||||
"""Class representing a Resource Dump Manifest.
|
||||
|
||||
@ -19,7 +20,15 @@ class ResourceDumpManifest(ResourceList):
|
||||
and implemented as a sub-class of ResourceList.
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, md=None, ln=None, uri=None, allow_multifile=None, mapper=None):
|
||||
def __init__(self, resources=None, md=None, ln=None,
|
||||
uri=None, allow_multifile=None, mapper=None):
|
||||
"""Initialize ResourceDumpManifest."""
|
||||
super(ResourceDumpManifest, self).__init__(resources=resources, md=md, ln=ln, uri=uri, mapper=mapper)
|
||||
super(
|
||||
ResourceDumpManifest,
|
||||
self).__init__(
|
||||
resources=resources,
|
||||
md=md,
|
||||
ln=ln,
|
||||
uri=uri,
|
||||
mapper=mapper)
|
||||
self.capability_name = 'resourcedump-manifest'
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
"""ResourceSync Resource List object.
|
||||
|
||||
A Resource List is a set of resources with some metadata for
|
||||
each resource. Comparison of resource lists from a source and
|
||||
A Resource List is a set of resources with some metadata for
|
||||
each resource. Comparison of resource lists from a source and
|
||||
a destination allows understanding of whether the two are in
|
||||
sync or whether some resources need to be updated at the
|
||||
destination.
|
||||
|
||||
There may also be metadata about the Resource List, and links
|
||||
to other ResourceSync documents. Metadata include the
|
||||
timestamp of the Resource List (md_at) and, optionally, the
|
||||
to other ResourceSync documents. Metadata include the
|
||||
timestamp of the Resource List (md_at) and, optionally, the
|
||||
timestamp when creation of the Resource List was completed
|
||||
(md_completed).at the top level. These include a creation timestamp
|
||||
(md_completed).at the top level. These include a creation timestamp
|
||||
(from) and links to the Capability List.
|
||||
|
||||
Described in specification at:
|
||||
@ -22,9 +22,9 @@ import os
|
||||
from datetime import datetime
|
||||
import re
|
||||
import sys
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import URLopener
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import URLopener
|
||||
|
||||
from .list_base_with_index import ListBaseWithIndex
|
||||
@ -48,7 +48,7 @@ class ResourceListDict(dict):
|
||||
return(iter(self._iter_next, None))
|
||||
|
||||
def _iter_next(self):
|
||||
if (len(self._iter_next_list)>0):
|
||||
if (len(self._iter_next_list) > 0):
|
||||
return(self[self._iter_next_list.pop()])
|
||||
else:
|
||||
return(None)
|
||||
@ -61,13 +61,15 @@ class ResourceListDict(dict):
|
||||
"""Add just a single resource."""
|
||||
uri = resource.uri
|
||||
if (uri in self and not replace):
|
||||
raise ResourceListDupeError("Attempt to add resource already in resource_list")
|
||||
self[uri]=resource
|
||||
raise ResourceListDupeError(
|
||||
"Attempt to add resource already in resource_list")
|
||||
self[uri] = resource
|
||||
|
||||
|
||||
class ResourceListOrdered(list):
|
||||
"""Alternative implementation of class to store resources in ResourceList.
|
||||
|
||||
FIXME - This is a rather inefficient implementation which involves
|
||||
FIXME - This is a rather inefficient implementation which involves
|
||||
scanning all resources to check for duplicates. Designed just to enable
|
||||
re-creation of examples in the spec. Something dictionary based would
|
||||
likely be better. Might be best to use OrderedDict but that is available
|
||||
@ -91,18 +93,21 @@ class ResourceListOrdered(list):
|
||||
for r in self:
|
||||
if (uri == r.uri):
|
||||
if (replace):
|
||||
r=resource
|
||||
r = resource
|
||||
return
|
||||
else:
|
||||
raise ResourceListDupeError("Attempt to add resource already in resource_list")
|
||||
raise ResourceListDupeError(
|
||||
"Attempt to add resource already in resource_list")
|
||||
# didn't find it in list, add to end
|
||||
self.append(resource)
|
||||
|
||||
|
||||
class ResourceListDupeError(Exception):
|
||||
"""Exception in case of duplicate resource."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ResourceList(ListBaseWithIndex):
|
||||
"""Class representing a ResourceList.
|
||||
|
||||
@ -114,22 +119,22 @@ class ResourceList(ListBaseWithIndex):
|
||||
A ResourceList will admit only one resource with any given URI.
|
||||
|
||||
Typical usage for a small ResourceList is:
|
||||
|
||||
|
||||
rl = ResourceList()
|
||||
rl.add( Resource(...) )
|
||||
rl.add( Resource(...) )
|
||||
print rl.as_xml()
|
||||
|
||||
The default storage is unordered but the iterator imposes a canonical
|
||||
order which is alphabetical by URI. If it is desired to have
|
||||
The default storage is unordered but the iterator imposes a canonical
|
||||
order which is alphabetical by URI. If it is desired to have
|
||||
resources listed in the order they are added then the ResourceDictOrdered
|
||||
class may be specified on creation:
|
||||
|
||||
rl = ResourceList( resources_class=ResourceDictOrdered )
|
||||
|
||||
|
||||
In normal use it is expected that any Resource List Index will be
|
||||
created automatically when writing out a large Resource List in
|
||||
multiple sitemap files. However, should it be necessary to
|
||||
multiple sitemap files. However, should it be necessary to
|
||||
explicitly create an index then this may be specified with:
|
||||
|
||||
rli = ResourceList( resources_class=ResourceDictOrdered )
|
||||
@ -138,11 +143,11 @@ class ResourceList(ListBaseWithIndex):
|
||||
See additional descriptions in ListBaseWithIndex and ListBase.
|
||||
"""
|
||||
|
||||
def __init__(self, resources=None, count=None, md=None, ln=None, uri=None,
|
||||
def __init__(self, resources=None, count=None, md=None, ln=None, uri=None,
|
||||
allow_multifile=None, mapper=None, resources_class=ResourceListDict):
|
||||
"""Initialize ResourceList."""
|
||||
super(ResourceList, self).__init__(resources=resources, count=count, md=md, ln=ln, uri=uri,
|
||||
capability_name = 'resourcelist',
|
||||
capability_name='resourcelist',
|
||||
allow_multifile=allow_multifile, mapper=mapper,
|
||||
resources_class=resources_class)
|
||||
|
||||
@ -154,15 +159,15 @@ class ResourceList(ListBaseWithIndex):
|
||||
"""
|
||||
if isinstance(resource, collections.Iterable):
|
||||
for r in resource:
|
||||
self.resources.add(r,replace)
|
||||
self.resources.add(r, replace)
|
||||
else:
|
||||
self.resources.add(resource,replace)
|
||||
self.resources.add(resource, replace)
|
||||
|
||||
def compare(self,src):
|
||||
def compare(self, src):
|
||||
"""Compare this ResourceList object with that specified as src.
|
||||
|
||||
The parameter src must also be a ResourceList object, it is assumed
|
||||
to be the source, and the current object is the destination. This
|
||||
to be the source, and the current object is the destination. This
|
||||
written to work for any objects in self and sc, provided that the
|
||||
== operator can be used to compare them.
|
||||
|
||||
@ -171,38 +176,38 @@ class ResourceList(ListBaseWithIndex):
|
||||
"""
|
||||
dst_iter = iter(self.resources)
|
||||
src_iter = iter(src.resources)
|
||||
same=ResourceList()
|
||||
updated=ResourceList()
|
||||
deleted=ResourceList()
|
||||
created=ResourceList()
|
||||
dst_cur=next(dst_iter,None)
|
||||
src_cur=next(src_iter,None)
|
||||
same = ResourceList()
|
||||
updated = ResourceList()
|
||||
deleted = ResourceList()
|
||||
created = ResourceList()
|
||||
dst_cur = next(dst_iter, None)
|
||||
src_cur = next(src_iter, None)
|
||||
while ((dst_cur is not None) and (src_cur is not None)):
|
||||
#print 'dst='+dst_cur+' src='+src_cur
|
||||
# print 'dst='+dst_cur+' src='+src_cur
|
||||
if (dst_cur.uri == src_cur.uri):
|
||||
if (dst_cur==src_cur):
|
||||
if (dst_cur == src_cur):
|
||||
same.add(dst_cur)
|
||||
else:
|
||||
updated.add(src_cur)
|
||||
dst_cur=next(dst_iter,None)
|
||||
src_cur=next(src_iter,None)
|
||||
dst_cur = next(dst_iter, None)
|
||||
src_cur = next(src_iter, None)
|
||||
elif (not src_cur or dst_cur.uri < src_cur.uri):
|
||||
deleted.add(dst_cur)
|
||||
dst_cur=next(dst_iter,None)
|
||||
dst_cur = next(dst_iter, None)
|
||||
elif (not dst_cur or dst_cur.uri > src_cur.uri):
|
||||
created.add(src_cur)
|
||||
src_cur=next(src_iter,None)
|
||||
src_cur = next(src_iter, None)
|
||||
else:
|
||||
raise Exception("this should not be possible")
|
||||
# what do we have leftover in src or dst lists?
|
||||
while (dst_cur is not None):
|
||||
deleted.add(dst_cur)
|
||||
dst_cur=next(dst_iter,None)
|
||||
dst_cur = next(dst_iter, None)
|
||||
while (src_cur is not None):
|
||||
created.add(src_cur)
|
||||
src_cur=next(src_iter,None)
|
||||
src_cur = next(src_iter, None)
|
||||
# have now gone through both lists
|
||||
return(same,updated,deleted,created)
|
||||
return(same, updated, deleted, created)
|
||||
|
||||
def has_md5(self):
|
||||
"""Return true if at least one contained resource-like object has md5 data."""
|
||||
|
||||
@ -5,9 +5,9 @@ import os.path
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
try: #python3
|
||||
try: # python3
|
||||
from urllib.request import URLopener
|
||||
except ImportError: #python2
|
||||
except ImportError: # python2
|
||||
from urllib import URLopener
|
||||
from defusedxml.ElementTree import parse
|
||||
|
||||
@ -17,10 +17,11 @@ from .sitemap import Sitemap
|
||||
from .utils import compute_md5_for_file
|
||||
from .w3c_datetime import datetime_to_str
|
||||
|
||||
|
||||
class ResourceListBuilder():
|
||||
"""ResourceListBuilder to create ResourceList objects.
|
||||
|
||||
Currently implements build from files on disk only.
|
||||
Currently implements build from files on disk only.
|
||||
|
||||
Attributes:
|
||||
- set_path set true to add path attribute for each resource
|
||||
@ -33,16 +34,17 @@ class ResourceListBuilder():
|
||||
alternatives to md5.
|
||||
"""
|
||||
|
||||
def __init__(self, mapper=None, set_md5=False, set_length=True, set_path=False):
|
||||
def __init__(self, mapper=None, set_md5=False,
|
||||
set_length=True, set_path=False):
|
||||
"""Create ResourceListBuilder object, optionally set options.
|
||||
|
||||
The mapper attribute must be set before a call to from_disk() in order to
|
||||
map between local filenames and URIs.
|
||||
|
||||
The following attributes may be set to determine information added to
|
||||
The following attributes may be set to determine information added to
|
||||
each Resource object based on the disk scan:
|
||||
- set_md5 - True to add md5 digests for each resource. This may add
|
||||
significant time to the scan process as each file has to be read to
|
||||
- set_md5 - True to add md5 digests for each resource. This may add
|
||||
significant time to the scan process as each file has to be read to
|
||||
compute the sum
|
||||
- set_length - False to not add length for each resources
|
||||
- set_path - True to add local path information for each file/resource
|
||||
@ -52,7 +54,7 @@ class ResourceListBuilder():
|
||||
self.set_md5 = set_md5
|
||||
self.set_length = set_length
|
||||
self.exclude_files = ['sitemap\d{0,5}.xml']
|
||||
self.exclude_dirs = ['CVS','.git']
|
||||
self.exclude_dirs = ['CVS', '.git']
|
||||
self.include_symlinks = False
|
||||
# Used internally only:
|
||||
self.logger = logging.getLogger('resync.resource_list_builder')
|
||||
@ -70,7 +72,8 @@ class ResourceListBuilder():
|
||||
try:
|
||||
self.compiled_exclude_files.append(re.compile(pattern))
|
||||
except re.error as e:
|
||||
raise ValueError("Bad python regex in exclude '%s': %s" % (pattern,str(e)))
|
||||
raise ValueError(
|
||||
"Bad python regex in exclude '%s': %s" % (pattern, str(e)))
|
||||
|
||||
def exclude_file(self, file):
|
||||
"""True if file should be exclude based on name pattern."""
|
||||
@ -82,18 +85,18 @@ class ResourceListBuilder():
|
||||
def from_disk(self, resource_list=None, paths=None):
|
||||
"""Create or extend resource_list with resources from disk scan.
|
||||
|
||||
Assumes very simple disk path to URL mapping (in self.mapping): chop
|
||||
Assumes very simple disk path to URL mapping (in self.mapping): chop
|
||||
path and replace with url_path. Returns the new or extended ResourceList
|
||||
object.
|
||||
|
||||
If a resource_list is specified then items are added to that rather
|
||||
than creating a new one.
|
||||
|
||||
If paths is specified then these are used instead of the set
|
||||
If paths is specified then these are used instead of the set
|
||||
of local paths in self.mapping.
|
||||
|
||||
Example usage with mapping start paths:
|
||||
|
||||
|
||||
mapper=Mapper('http://example.org/path','/path/to/files')
|
||||
rlb = ResourceListBuilder(mapper=mapper)
|
||||
m = rlb.from_disk()
|
||||
@ -104,7 +107,7 @@ class ResourceListBuilder():
|
||||
rlb = ResourceListBuilder(mapper=mapper)
|
||||
m = rlb.from_disk(paths=['/path/to/files/a','/path/to/files/b'])
|
||||
"""
|
||||
num=0
|
||||
num = 0
|
||||
# Either use resource_list passed in or make a new one
|
||||
if (resource_list is None):
|
||||
resource_list = ResourceList()
|
||||
@ -112,10 +115,11 @@ class ResourceListBuilder():
|
||||
self.compile_excludes()
|
||||
# Work out start paths from map if not explicitly specified
|
||||
if (paths is None):
|
||||
paths=[]
|
||||
paths = []
|
||||
for map in self.mapper.mappings:
|
||||
paths.append(map.dst_path)
|
||||
# Set start time unless already set (perhaps because building in chunks)
|
||||
# Set start time unless already set (perhaps because building in
|
||||
# chunks)
|
||||
if (resource_list.md_at is None):
|
||||
resource_list.md_at = datetime_to_str()
|
||||
# Run for each map in the mappings
|
||||
@ -131,16 +135,18 @@ class ResourceListBuilder():
|
||||
# sanity
|
||||
if (path is None or resource_list is None or self.mapper is None):
|
||||
raise ValueError("Must specify path, resource_list and mapper")
|
||||
# is path a directory or a file? for each file: create Resource object,
|
||||
# is path a directory or a file? for each file: create Resource object,
|
||||
# add, increment counter
|
||||
if os.path.isdir(path):
|
||||
num_files=0
|
||||
for dirpath, dirs, files in os.walk(path,topdown=True):
|
||||
num_files = 0
|
||||
for dirpath, dirs, files in os.walk(path, topdown=True):
|
||||
for file_in_dirpath in files:
|
||||
num_files+=1
|
||||
if (num_files%50000 == 0):
|
||||
self.logger.info("ResourceListBuilder.from_disk_add_path: %d files..." % (num_files))
|
||||
self.add_file(resource_list=resource_list,dir=dirpath,file=file_in_dirpath)
|
||||
num_files += 1
|
||||
if (num_files % 50000 == 0):
|
||||
self.logger.info(
|
||||
"ResourceListBuilder.from_disk_add_path: %d files..." % (num_files))
|
||||
self.add_file(resource_list=resource_list,
|
||||
dir=dirpath, file=file_in_dirpath)
|
||||
# prune list of dirs based on self.exclude_dirs
|
||||
for exclude in self.exclude_dirs:
|
||||
if exclude in dirs:
|
||||
@ -148,11 +154,11 @@ class ResourceListBuilder():
|
||||
dirs.remove(exclude)
|
||||
else:
|
||||
# single file
|
||||
self.add_file(resource_list=resource_list,file=path)
|
||||
self.add_file(resource_list=resource_list, file=path)
|
||||
|
||||
def add_file(self, resource_list=None, dir=None, file=None):
|
||||
"""Add a single file to resource_list.
|
||||
|
||||
|
||||
Follows object settings of set_path, set_md5 and set_length.
|
||||
"""
|
||||
try:
|
||||
@ -161,25 +167,26 @@ class ResourceListBuilder():
|
||||
return
|
||||
# get abs filename and also URL
|
||||
if (dir is not None):
|
||||
file = os.path.join(dir,file)
|
||||
if (not os.path.isfile(file) or not (self.include_symlinks or not os.path.islink(file))):
|
||||
file = os.path.join(dir, file)
|
||||
if (not os.path.isfile(file) or not (
|
||||
self.include_symlinks or not os.path.islink(file))):
|
||||
return
|
||||
uri = self.mapper.dst_to_src(file)
|
||||
if (uri is None):
|
||||
raise Exception("Internal error, mapping failed")
|
||||
file_stat=os.stat(file)
|
||||
file_stat = os.stat(file)
|
||||
except OSError as e:
|
||||
sys.stderr.write("Ignoring file %s (error: %s)" % (file,str(e)))
|
||||
sys.stderr.write("Ignoring file %s (error: %s)" % (file, str(e)))
|
||||
return
|
||||
timestamp = file_stat.st_mtime #UTC
|
||||
r = Resource(uri=uri,timestamp=timestamp)
|
||||
timestamp = file_stat.st_mtime # UTC
|
||||
r = Resource(uri=uri, timestamp=timestamp)
|
||||
if (self.set_path):
|
||||
# add full local path
|
||||
r.path=file
|
||||
r.path = file
|
||||
if (self.set_md5):
|
||||
# add md5
|
||||
r.md5=compute_md5_for_file(file)
|
||||
r.md5 = compute_md5_for_file(file)
|
||||
if (self.set_length):
|
||||
# add length
|
||||
r.length=file_stat.st_size
|
||||
r.length = file_stat.st_size
|
||||
resource_list.add(r)
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
"""Base class representing a set of Resource objects."""
|
||||
|
||||
|
||||
class ResourceSet(dict):
|
||||
"""Base class representing a set of Resource objects.
|
||||
|
||||
The ResourceSet class is used for Capability List Indexes and
|
||||
The ResourceSet class is used for Capability List Indexes and
|
||||
ResourceSync Description documents.
|
||||
|
||||
Ordering of Resources is currently alphanumeric (using sorted(..))
|
||||
on the uri which is the key.
|
||||
on the uri which is the key.
|
||||
|
||||
Key properties of this class are:
|
||||
- has add(resource) method
|
||||
@ -21,7 +22,7 @@ class ResourceSet(dict):
|
||||
return(iter(self._iter_next, None))
|
||||
|
||||
def _iter_next(self):
|
||||
if (len(self._iter_next_list)>0):
|
||||
if (len(self._iter_next_list) > 0):
|
||||
return(self[self._iter_next_list.pop()])
|
||||
else:
|
||||
return(None)
|
||||
@ -30,8 +31,10 @@ class ResourceSet(dict):
|
||||
"""Add just a single resource."""
|
||||
uri = resource.uri
|
||||
if (uri in self and not replace):
|
||||
raise ResourceSetDupeError("Attempt to add resource already in this set")
|
||||
self[uri]=resource
|
||||
raise ResourceSetDupeError(
|
||||
"Attempt to add resource already in this set")
|
||||
self[uri] = resource
|
||||
|
||||
|
||||
class ResourceSetDupeError(Exception):
|
||||
"""Exception for case of attempt to add duplicate resource."""
|
||||
|
||||
@ -6,11 +6,11 @@ import sys
|
||||
import logging
|
||||
from defusedxml.ElementTree import parse
|
||||
from xml.etree.ElementTree import ElementTree, Element, tostring
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
|
||||
from .resource import Resource
|
||||
@ -27,6 +27,7 @@ XML_ATT_NAME = {
|
||||
'md_until': 'until'
|
||||
}
|
||||
|
||||
|
||||
class SitemapIndexError(Exception):
|
||||
"""Exception if sitemapindex read instead of sitemap or vice-versa.
|
||||
|
||||
@ -43,21 +44,24 @@ class SitemapIndexError(Exception):
|
||||
"""Return just the message attribute."""
|
||||
return(self.message)
|
||||
|
||||
|
||||
class SitemapParseError(Exception):
|
||||
"""Exception for sitemap parsing structural error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SitemapDupeError(Exception):
|
||||
"""Exception for case of duplicate resources in sitemap."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Sitemap(object):
|
||||
"""Read and write sitemaps.
|
||||
|
||||
Implemented as a separate class that uses ResourceContainer
|
||||
(ResourceList or ChangeList) and Resource classes as data objects.
|
||||
Implemented as a separate class that uses ResourceContainer
|
||||
(ResourceList or ChangeList) and Resource classes as data objects.
|
||||
Reads and write sitemaps, including multiple file sitemaps.
|
||||
|
||||
This class does not automatically handle the reading or writing
|
||||
@ -69,19 +73,18 @@ class Sitemap(object):
|
||||
def __init__(self, pretty_xml=False):
|
||||
"""Initialize Sitemap object."""
|
||||
self.logger = logging.getLogger('resync.sitemap')
|
||||
self.pretty_xml=pretty_xml
|
||||
self.pretty_xml = pretty_xml
|
||||
# Classes used when parsing
|
||||
self.resource_class=Resource
|
||||
self.resource_class = Resource
|
||||
# Information recorded for logging
|
||||
self.resources_created=0 # Set during parsing sitemap
|
||||
self.parsed_index=None # Set True for sitemapindex, False for sitemap
|
||||
self.resources_created = 0 # Set during parsing sitemap
|
||||
self.parsed_index = None # Set True for sitemapindex, False for sitemap
|
||||
|
||||
|
||||
##### Write the XML for a sitemap or sitemapindex #####
|
||||
# Write the XML for a sitemap or sitemapindex
|
||||
|
||||
def resources_as_xml(self, resources, sitemapindex=False, fh=None):
|
||||
"""Write or return XML for a set of resources in sitemap format.
|
||||
|
||||
|
||||
Arguments:
|
||||
- resources - either an iterable or iterator of Resource objects;
|
||||
if there an md attribute this will go to <rs:md>
|
||||
@ -90,51 +93,59 @@ class Sitemap(object):
|
||||
- fh - write to filehandle fh instead of returning string
|
||||
"""
|
||||
# element names depending on sitemapindex or not
|
||||
root_element = ( 'sitemapindex' if (sitemapindex) else 'urlset' )
|
||||
item_element = ( 'sitemap' if (sitemapindex) else 'url')
|
||||
root_element = ('sitemapindex' if (sitemapindex) else 'urlset')
|
||||
item_element = ('sitemap' if (sitemapindex) else 'url')
|
||||
# namespaces and other settings
|
||||
namespaces = { 'xmlns': SITEMAP_NS, 'xmlns:rs': RS_NS }
|
||||
namespaces = {'xmlns': SITEMAP_NS, 'xmlns:rs': RS_NS}
|
||||
root = Element(root_element, namespaces)
|
||||
if (self.pretty_xml):
|
||||
root.text="\n"
|
||||
root.text = "\n"
|
||||
# <rs:ln>
|
||||
if (hasattr(resources,'ln')):
|
||||
if (hasattr(resources, 'ln')):
|
||||
for ln in resources.ln:
|
||||
self.add_element_with_atts_to_etree(root,'rs:ln',ln)
|
||||
self.add_element_with_atts_to_etree(root, 'rs:ln', ln)
|
||||
# <rs:md>
|
||||
if (hasattr(resources,'md')):
|
||||
self.add_element_with_atts_to_etree(root,'rs:md',resources.md)
|
||||
if (hasattr(resources, 'md')):
|
||||
self.add_element_with_atts_to_etree(root, 'rs:md', resources.md)
|
||||
# <url> entries from either an iterable or an iterator
|
||||
for r in resources:
|
||||
e=self.resource_etree_element(r, element_name=item_element)
|
||||
e = self.resource_etree_element(r, element_name=item_element)
|
||||
root.append(e)
|
||||
# have tree, now serialize
|
||||
tree = ElementTree(root);
|
||||
xml_buf=None
|
||||
tree = ElementTree(root)
|
||||
xml_buf = None
|
||||
if (fh is None):
|
||||
xml_buf=io.StringIO()
|
||||
fh=xml_buf
|
||||
if (sys.version_info < (2,7)):
|
||||
tree.write(fh,encoding='UTF-8')
|
||||
elif (sys.version_info < (3,0)):
|
||||
tree.write(fh,encoding='UTF-8',xml_declaration=True,method='xml')
|
||||
xml_buf = io.StringIO()
|
||||
fh = xml_buf
|
||||
if (sys.version_info < (2, 7)):
|
||||
tree.write(fh, encoding='UTF-8')
|
||||
elif (sys.version_info < (3, 0)):
|
||||
tree.write(
|
||||
fh,
|
||||
encoding='UTF-8',
|
||||
xml_declaration=True,
|
||||
method='xml')
|
||||
else:
|
||||
tree.write(fh,encoding="unicode",xml_declaration=True,method='xml')
|
||||
tree.write(
|
||||
fh,
|
||||
encoding="unicode",
|
||||
xml_declaration=True,
|
||||
method='xml')
|
||||
if (xml_buf is not None):
|
||||
return(xml_buf.getvalue())
|
||||
|
||||
# Read/parse an XML sitemap or sitemapindex
|
||||
|
||||
##### Read/parse an XML sitemap or sitemapindex #####
|
||||
|
||||
def parse_xml(self, fh=None, etree=None, resources=None, capability=None, sitemapindex=None):
|
||||
def parse_xml(self, fh=None, etree=None, resources=None,
|
||||
capability=None, sitemapindex=None):
|
||||
"""Parse XML Sitemap and add to resources object.
|
||||
|
||||
Reads from fh or etree and adds resources to a resorces object
|
||||
Reads from fh or etree and adds resources to a resorces object
|
||||
(which must support the add method). Returns the resources object.
|
||||
|
||||
Also sets self.resources_created to be the number of resources created.
|
||||
We adopt a very lax approach here. The parsing is properly namespace
|
||||
aware but we search just for the elements wanted and leave everything
|
||||
Also sets self.resources_created to be the number of resources created.
|
||||
We adopt a very lax approach here. The parsing is properly namespace
|
||||
aware but we search just for the elements wanted and leave everything
|
||||
else alone.
|
||||
|
||||
This method will read either sitemap or sitemapindex documents. Behavior
|
||||
@ -143,34 +154,38 @@ class Sitemap(object):
|
||||
- False - SitemapIndexError exception if sitemapindex detected
|
||||
- True - SitemapIndexError exception if sitemap detected
|
||||
|
||||
Will set self.parsed_index based on whether a sitemap or sitemapindex
|
||||
Will set self.parsed_index based on whether a sitemap or sitemapindex
|
||||
document was read:
|
||||
- False - sitemap
|
||||
- True - sitemapindex
|
||||
"""
|
||||
if (resources is None):
|
||||
resources=ResourceContainer()
|
||||
resources = ResourceContainer()
|
||||
if (fh is not None):
|
||||
etree=parse(fh)
|
||||
etree = parse(fh)
|
||||
elif (etree is None):
|
||||
raise ValueError("Neither fh or etree set")
|
||||
# check root element: urlset (for sitemap), sitemapindex or bad
|
||||
root_tag = etree.getroot().tag
|
||||
resource_tag = None # will be <url> or <sitemap> depending on type
|
||||
resource_tag = None # will be <url> or <sitemap> depending on type
|
||||
self.parsed_index = None
|
||||
if (root_tag == '{'+SITEMAP_NS+"}urlset"):
|
||||
if (root_tag == '{' + SITEMAP_NS + "}urlset"):
|
||||
self.parsed_index = False
|
||||
if (sitemapindex is not None and sitemapindex):
|
||||
raise SitemapIndexError("Got sitemap when expecting sitemapindex",etree)
|
||||
resource_tag = '{'+SITEMAP_NS+"}url"
|
||||
elif (root_tag == '{'+SITEMAP_NS+"}sitemapindex"):
|
||||
raise SitemapIndexError(
|
||||
"Got sitemap when expecting sitemapindex", etree)
|
||||
resource_tag = '{' + SITEMAP_NS + "}url"
|
||||
elif (root_tag == '{' + SITEMAP_NS + "}sitemapindex"):
|
||||
self.parsed_index = True
|
||||
if (sitemapindex is not None and not sitemapindex):
|
||||
raise SitemapIndexError("Got sitemapindex when expecting sitemap",etree)
|
||||
resource_tag = '{'+SITEMAP_NS+"}sitemap"
|
||||
raise SitemapIndexError(
|
||||
"Got sitemapindex when expecting sitemap", etree)
|
||||
resource_tag = '{' + SITEMAP_NS + "}sitemap"
|
||||
else:
|
||||
raise SitemapParseError("XML is not sitemap or sitemapindex (root element is <%s>)" % root_tag)
|
||||
|
||||
raise SitemapParseError(
|
||||
"XML is not sitemap or sitemapindex (root element is <%s>)" %
|
||||
root_tag)
|
||||
|
||||
# have what we expect, read it
|
||||
in_preamble = True
|
||||
self.resources_created = 0
|
||||
@ -179,27 +194,32 @@ class Sitemap(object):
|
||||
# look for <rs:md> and <rs:ln>, first <url> ends
|
||||
# then look for resources in <url> blocks
|
||||
if (e.tag == resource_tag):
|
||||
in_preamble = False #any later rs:md or rs:ln is error
|
||||
in_preamble = False # any later rs:md or rs:ln is error
|
||||
r = self.resource_from_etree(e, self.resource_class)
|
||||
try:
|
||||
resources.add( r )
|
||||
resources.add(r)
|
||||
except SitemapDupeError:
|
||||
self.logger.warning("dupe of: %s (lastmod=%s)" % (r.uri,r.lastmod))
|
||||
self.resources_created+=1
|
||||
elif (e.tag == "{"+RS_NS+"}md"):
|
||||
self.logger.warning(
|
||||
"dupe of: %s (lastmod=%s)" %
|
||||
(r.uri, r.lastmod))
|
||||
self.resources_created += 1
|
||||
elif (e.tag == "{" + RS_NS + "}md"):
|
||||
if (in_preamble):
|
||||
if (seen_top_level_md):
|
||||
raise SitemapParseError("Multiple <rs:md> at top level of sitemap")
|
||||
raise SitemapParseError(
|
||||
"Multiple <rs:md> at top level of sitemap")
|
||||
else:
|
||||
resources.md = self.md_from_etree(e,'preamble')
|
||||
resources.md = self.md_from_etree(e, 'preamble')
|
||||
seen_top_level_md = True
|
||||
else:
|
||||
raise SitemapParseError("Found <rs:md> after first <url> in sitemap")
|
||||
elif (e.tag == "{"+RS_NS+"}ln"):
|
||||
raise SitemapParseError(
|
||||
"Found <rs:md> after first <url> in sitemap")
|
||||
elif (e.tag == "{" + RS_NS + "}ln"):
|
||||
if (in_preamble):
|
||||
resources.ln.append(self.ln_from_etree(e,'preamble'))
|
||||
resources.ln.append(self.ln_from_etree(e, 'preamble'))
|
||||
else:
|
||||
raise SitemapParseError("Found <rs:ln> after first <url> in sitemap")
|
||||
raise SitemapParseError(
|
||||
"Found <rs:ln> after first <url> in sitemap")
|
||||
else:
|
||||
# element we don't recognize, ignore
|
||||
pass
|
||||
@ -207,71 +227,71 @@ class Sitemap(object):
|
||||
if (capability is not None):
|
||||
if ('capability' not in resources.md):
|
||||
if (capability == 'resourcelist'):
|
||||
self.logger.warning('No capability specified in sitemap, assuming resourcelist')
|
||||
self.logger.warning(
|
||||
'No capability specified in sitemap, assuming resourcelist')
|
||||
resources.md['capability'] = 'resourcelist'
|
||||
else:
|
||||
raise SitemapParseError("Expected to read a %s document, but no capability specified in sitemap" %
|
||||
(capability))
|
||||
(capability))
|
||||
if (resources.md['capability'] != capability):
|
||||
raise SitemapParseError("Expected to read a %s document, got %s" %
|
||||
(capability,resources.md['capability']))
|
||||
(capability, resources.md['capability']))
|
||||
# return the resource container object
|
||||
return(resources)
|
||||
|
||||
|
||||
##### Resource methods #####
|
||||
# Resource methods
|
||||
|
||||
def resource_etree_element(self, resource, element_name='url'):
|
||||
"""Return xml.etree.ElementTree.Element representing the resource.
|
||||
|
||||
Returns and element for the specified resource, of the form <url>
|
||||
Returns and element for the specified resource, of the form <url>
|
||||
with enclosed properties that are based on the sitemap with extensions
|
||||
for ResourceSync.
|
||||
"""
|
||||
e = Element(element_name)
|
||||
sub = Element('loc')
|
||||
sub.text=resource.uri
|
||||
sub.text = resource.uri
|
||||
e.append(sub)
|
||||
if (resource.timestamp is not None):
|
||||
# Create appriate element for timestamp
|
||||
sub = Element('lastmod')
|
||||
sub.text = str(resource.lastmod) #W3C Datetime in UTC
|
||||
sub.text = str(resource.lastmod) # W3C Datetime in UTC
|
||||
e.append(sub)
|
||||
md_atts = {}
|
||||
for att in ('capability','change','hash','length','path','mime_type',
|
||||
'md_at','md_completed','md_from','md_until'):
|
||||
val = getattr(resource,att,None)
|
||||
for att in ('capability', 'change', 'hash', 'length', 'path', 'mime_type',
|
||||
'md_at', 'md_completed', 'md_from', 'md_until'):
|
||||
val = getattr(resource, att, None)
|
||||
if (val is not None):
|
||||
md_atts[self._xml_att_name(att)] = str(val)
|
||||
if (len(md_atts)>0):
|
||||
md = Element('rs:md',md_atts)
|
||||
if (len(md_atts) > 0):
|
||||
md = Element('rs:md', md_atts)
|
||||
e.append(md)
|
||||
# add any <rs:ln>
|
||||
if (hasattr(resource,'ln') and
|
||||
resource.ln is not None):
|
||||
if (hasattr(resource, 'ln') and
|
||||
resource.ln is not None):
|
||||
for ln in resource.ln:
|
||||
self.add_element_with_atts_to_etree(e,'rs:ln',ln)
|
||||
self.add_element_with_atts_to_etree(e, 'rs:ln', ln)
|
||||
if (self.pretty_xml):
|
||||
e.tail="\n"
|
||||
e.tail = "\n"
|
||||
return(e)
|
||||
|
||||
def resource_as_xml(self,resource):
|
||||
def resource_as_xml(self, resource):
|
||||
"""Return string for the resource as part of an XML sitemap.
|
||||
|
||||
Returns a string with the XML snippet representing the resource,
|
||||
without any XML declaration.
|
||||
"""
|
||||
e = self.resource_etree_element(resource)
|
||||
if (sys.version_info > (3,0)):
|
||||
if (sys.version_info > (3, 0)):
|
||||
# python3.x
|
||||
return(tostring(e, encoding='unicode', method='xml'))
|
||||
elif (sys.version_info >= (2,7)):
|
||||
elif (sys.version_info >= (2, 7)):
|
||||
s = tostring(e, encoding='UTF-8', method='xml')
|
||||
else:
|
||||
#must not specify method='xml' in python2.6
|
||||
# must not specify method='xml' in python2.6
|
||||
s = tostring(e, encoding='UTF-8')
|
||||
# Chop off XML declaration that is added in 2.x... sigh
|
||||
return(s.replace("<?xml version='1.0' encoding='UTF-8'?>\n",''))
|
||||
return(s.replace("<?xml version='1.0' encoding='UTF-8'?>\n", ''))
|
||||
|
||||
def resource_from_etree(self, etree, resource_class):
|
||||
"""Construct a Resource from an etree.
|
||||
@ -280,56 +300,61 @@ class Sitemap(object):
|
||||
etree - the etree to parse
|
||||
resource_class - class of Resource object to create
|
||||
|
||||
The parsing is properly namespace aware but we search just
|
||||
for the elements wanted and leave everything else alone. Will
|
||||
The parsing is properly namespace aware but we search just
|
||||
for the elements wanted and leave everything else alone. Will
|
||||
raise an error if there are multiple <loc> or multiple <lastmod>
|
||||
elements. Otherwise, provided there is a <loc> element then will
|
||||
elements. Otherwise, provided there is a <loc> element then will
|
||||
go ahead and extract as much as possible.
|
||||
|
||||
All errors raised are SitemapParseError with messages intended
|
||||
to help debug problematic sitemap XML.
|
||||
"""
|
||||
loc_elements = etree.findall('{'+SITEMAP_NS+"}loc")
|
||||
if (len(loc_elements)>1):
|
||||
raise SitemapParseError("Multiple <loc> elements while parsing <url> in sitemap");
|
||||
elif (len(loc_elements)==0):
|
||||
raise SitemapParseError("Missing <loc> element while parsing <url> in sitemap")
|
||||
loc_elements = etree.findall('{' + SITEMAP_NS + "}loc")
|
||||
if (len(loc_elements) > 1):
|
||||
raise SitemapParseError(
|
||||
"Multiple <loc> elements while parsing <url> in sitemap")
|
||||
elif (len(loc_elements) == 0):
|
||||
raise SitemapParseError(
|
||||
"Missing <loc> element while parsing <url> in sitemap")
|
||||
else:
|
||||
loc = loc_elements[0].text
|
||||
if (loc is None or loc==''):
|
||||
raise SitemapParseError("Bad <loc> element with no content while parsing <url> in sitemap")
|
||||
if (loc is None or loc == ''):
|
||||
raise SitemapParseError(
|
||||
"Bad <loc> element with no content while parsing <url> in sitemap")
|
||||
# must at least have a URI, make this object
|
||||
resource=resource_class(uri=loc)
|
||||
resource = resource_class(uri=loc)
|
||||
# and hopefully a lastmod datetime (but none is OK)
|
||||
lastmod_elements = etree.findall('{'+SITEMAP_NS+"}lastmod")
|
||||
if (len(lastmod_elements)>1):
|
||||
raise SitemapParseError("Multiple <lastmod> elements while parsing <url> in sitemap");
|
||||
elif (len(lastmod_elements)==1):
|
||||
resource.lastmod=lastmod_elements[0].text
|
||||
lastmod_elements = etree.findall('{' + SITEMAP_NS + "}lastmod")
|
||||
if (len(lastmod_elements) > 1):
|
||||
raise SitemapParseError(
|
||||
"Multiple <lastmod> elements while parsing <url> in sitemap")
|
||||
elif (len(lastmod_elements) == 1):
|
||||
resource.lastmod = lastmod_elements[0].text
|
||||
# proceed to look for other resource attributes in an rs:md element
|
||||
md_elements = etree.findall('{'+RS_NS+"}md")
|
||||
if (len(md_elements)>1):
|
||||
raise SitemapParseError("Found multiple (%d) <rs:md> elements for %s", (len(md_elements),loc))
|
||||
elif (len(md_elements)==1):
|
||||
md_elements = etree.findall('{' + RS_NS + "}md")
|
||||
if (len(md_elements) > 1):
|
||||
raise SitemapParseError(
|
||||
"Found multiple (%d) <rs:md> elements for %s", (len(md_elements), loc))
|
||||
elif (len(md_elements) == 1):
|
||||
# have on element, look at attributes
|
||||
md = self.md_from_etree(md_elements[0],context=loc)
|
||||
md = self.md_from_etree(md_elements[0], context=loc)
|
||||
# simple attributes that map directly to Resource object attributes
|
||||
for att in ('capability','change','length','path','mime_type'):
|
||||
for att in ('capability', 'change', 'length', 'path', 'mime_type'):
|
||||
if (att in md):
|
||||
setattr(resource,att,md[att])
|
||||
setattr(resource, att, md[att])
|
||||
# The ResourceSync beta spec lists md5, sha-1 and sha-256 fixity
|
||||
# digest types. Parse and warn of errors ignored.
|
||||
if ('hash' in md):
|
||||
try:
|
||||
resource.hash = md['hash']
|
||||
except ValueError as e:
|
||||
self.logger.warning("%s in <rs:md> for %s" % (str(e),loc))
|
||||
self.logger.warning("%s in <rs:md> for %s" % (str(e), loc))
|
||||
# look for rs:ln elements (optional)
|
||||
ln_elements = etree.findall('{'+RS_NS+"}ln")
|
||||
if (len(ln_elements)>0):
|
||||
ln_elements = etree.findall('{' + RS_NS + "}ln")
|
||||
if (len(ln_elements) > 0):
|
||||
resource.ln = []
|
||||
for ln_element in ln_elements:
|
||||
resource.ln.append(self.ln_from_etree(ln_element,loc))
|
||||
resource.ln.append(self.ln_from_etree(ln_element, loc))
|
||||
return(resource)
|
||||
|
||||
def md_from_etree(self, md_element, context=''):
|
||||
@ -341,31 +366,36 @@ class Sitemap(object):
|
||||
"""
|
||||
md = {}
|
||||
# grab all understood attributes into md dict
|
||||
for att in ('capability','change','hash','length','path','mime_type',
|
||||
'md_at','md_completed','md_from','md_until'):
|
||||
for att in ('capability', 'change', 'hash', 'length', 'path', 'mime_type',
|
||||
'md_at', 'md_completed', 'md_from', 'md_until'):
|
||||
xml_att = self._xml_att_name(att)
|
||||
val = md_element.attrib.get(xml_att,None)
|
||||
val = md_element.attrib.get(xml_att, None)
|
||||
if (val is not None):
|
||||
md[att] = val
|
||||
# capability. Allow this to be missing but do a very simple syntax
|
||||
# check on plausible values if present
|
||||
if ('capability' in md):
|
||||
if (re.match(r"^[\w\-]+$", md['capability']) is None):
|
||||
raise SitemapParseError("Bad capability name '%s' in %s" % (capability,context))
|
||||
raise SitemapParseError(
|
||||
"Bad capability name '%s' in %s" %
|
||||
(capability, context))
|
||||
# change should be one of defined values
|
||||
if ('change' in md):
|
||||
if (md['change'] not in ['created','updated','deleted'] ):
|
||||
self.logger.warning("Bad change attribute in <rs:md> for %s" % (context))
|
||||
if (md['change'] not in ['created', 'updated', 'deleted']):
|
||||
self.logger.warning(
|
||||
"Bad change attribute in <rs:md> for %s" %
|
||||
(context))
|
||||
# length should be an integer
|
||||
if ('length' in md):
|
||||
try:
|
||||
md['length']=int(md['length'])
|
||||
md['length'] = int(md['length'])
|
||||
except ValueError as e:
|
||||
raise SitemapParseError("Invalid length element in <rs:md> for %s" % (context))
|
||||
raise SitemapParseError(
|
||||
"Invalid length element in <rs:md> for %s" %
|
||||
(context))
|
||||
return(md)
|
||||
|
||||
|
||||
def ln_from_etree(self,ln_element,context=''):
|
||||
def ln_from_etree(self, ln_element, context=''):
|
||||
"""Parse rs:ln element from an etree, returning a dict of the data.
|
||||
|
||||
Parameters:
|
||||
@ -374,36 +404,44 @@ class Sitemap(object):
|
||||
"""
|
||||
ln = {}
|
||||
# grab all understood attributes into ln dict
|
||||
for att in ('hash','href','length','modified','path','rel','pri','mime_type'):
|
||||
for att in ('hash', 'href', 'length', 'modified',
|
||||
'path', 'rel', 'pri', 'mime_type'):
|
||||
xml_att = self._xml_att_name(att)
|
||||
val = ln_element.attrib.get(xml_att,None)
|
||||
val = ln_element.attrib.get(xml_att, None)
|
||||
if (val is not None):
|
||||
ln[att] = val
|
||||
# now do some checks and conversions...
|
||||
# href (MANDATORY)
|
||||
if ('href' not in ln):
|
||||
raise SitemapParseError("Missing href in <rs:ln> in %s" % (context))
|
||||
raise SitemapParseError(
|
||||
"Missing href in <rs:ln> in %s" %
|
||||
(context))
|
||||
# rel (MANDATORY)
|
||||
if ('rel' not in ln):
|
||||
raise SitemapParseError("Missing rel in <rs:ln> in %s" % (context))
|
||||
# length in bytes
|
||||
if ('length' in ln):
|
||||
try:
|
||||
ln['length']=int(ln['length'])
|
||||
ln['length'] = int(ln['length'])
|
||||
except ValueError as e:
|
||||
raise SitemapParseError("Invalid length attribute value in <rs:ln> for %s" % (context))
|
||||
raise SitemapParseError(
|
||||
"Invalid length attribute value in <rs:ln> for %s" %
|
||||
(context))
|
||||
# pri - priority, must be a number between 1 and 999999
|
||||
if ('pri' in ln):
|
||||
try:
|
||||
ln['pri']=int(ln['pri'])
|
||||
ln['pri'] = int(ln['pri'])
|
||||
except ValueError as e:
|
||||
raise SitemapParseError("Invalid pri attribute in <rs:ln> for %s" % (context))
|
||||
if (ln['pri']<1 or ln['pri']>999999):
|
||||
raise SitemapParseError("Bad pri attribute value in <rs:ln> for %s" % (context))
|
||||
raise SitemapParseError(
|
||||
"Invalid pri attribute in <rs:ln> for %s" %
|
||||
(context))
|
||||
if (ln['pri'] < 1 or ln['pri'] > 999999):
|
||||
raise SitemapParseError(
|
||||
"Bad pri attribute value in <rs:ln> for %s" %
|
||||
(context))
|
||||
return(ln)
|
||||
|
||||
|
||||
##### Metadata and link elements #####
|
||||
# Metadata and link elements
|
||||
|
||||
def add_element_with_atts_to_etree(self, etree, name, atts):
|
||||
"""Add element with name and atts to etree iff there are any atts.
|
||||
@ -418,18 +456,18 @@ class Sitemap(object):
|
||||
val = atts[att]
|
||||
if (val is not None):
|
||||
xml_atts[self._xml_att_name(att)] = str(val)
|
||||
if (len(xml_atts)>0):
|
||||
e = Element(name,xml_atts)
|
||||
if (len(xml_atts) > 0):
|
||||
e = Element(name, xml_atts)
|
||||
if (self.pretty_xml):
|
||||
e.tail="\n"
|
||||
e.tail = "\n"
|
||||
etree.append(e)
|
||||
|
||||
def _xml_att_name(self,att):
|
||||
def _xml_att_name(self, att):
|
||||
"""Get XML attribute name corresponding to supplied Resource object attribute.
|
||||
|
||||
We cannot use the XML attribute names in Python because 'from' and others
|
||||
conflict with Python reserved words. To be consistent all the extra timestamps
|
||||
are prefixed with md_. Assumption is that attibute name is same unless
|
||||
are prefixed with md_. Assumption is that attibute name is same unless
|
||||
specified in XML_ATT_NAME.
|
||||
"""
|
||||
return XML_ATT_NAME.get(att,att)
|
||||
return XML_ATT_NAME.get(att, att)
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
"""ResourceSync Source Description object.
|
||||
|
||||
A ResourceSync Source Description enumerates the Capability
|
||||
Lists offered by a Source. Since a Source has one Capability
|
||||
List per set of resources that it distinguishes, the
|
||||
ResourceSync Source Description will enumerate as many
|
||||
A ResourceSync Source Description enumerates the Capability
|
||||
Lists offered by a Source. Since a Source has one Capability
|
||||
List per set of resources that it distinguishes, the
|
||||
ResourceSync Source Description will enumerate as many
|
||||
Capability Lists as the Source has distinct sets of resources.
|
||||
|
||||
The ResourceSync Source Description is based on the <urlset>
|
||||
format. The specification allows for a very large Source Descriptions
|
||||
to use and index based on a <sitemapindex>, though this is probably
|
||||
to use and index based on a <sitemapindex>, though this is probably
|
||||
rarely necessary!
|
||||
|
||||
There is no meaning in the order of description of sets of resources
|
||||
There is no meaning in the order of description of sets of resources
|
||||
in a Source Description so the default is to store these descriptions
|
||||
as a set (ResourceSet).
|
||||
|
||||
@ -27,6 +27,7 @@ from resync.resource import Resource
|
||||
from resync.resource_set import ResourceSet
|
||||
from resync.list_base_with_index import ListBaseWithIndex
|
||||
|
||||
|
||||
class SourceDescription(ListBaseWithIndex):
|
||||
"""Class representing the set of Capability Lists supported.
|
||||
|
||||
@ -42,7 +43,7 @@ class SourceDescription(ListBaseWithIndex):
|
||||
super(SourceDescription, self).__init__(resources=resources, md=md, ln=ln,
|
||||
capability_name='description',
|
||||
resources_class=ResourceSet)
|
||||
self.md['from']=None #usually don't want a from date
|
||||
self.md['from'] = None # usually don't want a from date
|
||||
|
||||
def add(self, resource, replace=False):
|
||||
"""Add a resource or an iterable collection of resources.
|
||||
@ -52,23 +53,23 @@ class SourceDescription(ListBaseWithIndex):
|
||||
"""
|
||||
if isinstance(resource, collections.Iterable):
|
||||
for r in resource:
|
||||
self.resources.add(r,replace)
|
||||
self.resources.add(r, replace)
|
||||
else:
|
||||
self.resources.add(resource,replace)
|
||||
self.resources.add(resource, replace)
|
||||
|
||||
def add_capability_list(self,capability_list=None):
|
||||
def add_capability_list(self, capability_list=None):
|
||||
"""Add a capability list.
|
||||
|
||||
Adds either a CapabiltyList object specified in capability_list
|
||||
or else creates a Resource with the URI given in capability_list
|
||||
and adds that to the Source Description
|
||||
"""
|
||||
if (hasattr(capability_list,'uri')):
|
||||
r = Resource( uri=capability_list.uri,
|
||||
capability=capability_list.capability_name )
|
||||
if (hasattr(capability_list, 'uri')):
|
||||
r = Resource(uri=capability_list.uri,
|
||||
capability=capability_list.capability_name)
|
||||
if (capability_list.describedby is not None):
|
||||
r.link_set( rel='describedby', href=capability_list.describedby )
|
||||
r.link_set(rel='describedby', href=capability_list.describedby)
|
||||
else:
|
||||
r = Resource( uri=capability_list,
|
||||
capability='capabilitylist')
|
||||
self.add( r )
|
||||
r = Resource(uri=capability_list,
|
||||
capability='capabilitylist')
|
||||
self.add(r)
|
||||
|
||||
@ -113,7 +113,7 @@ def str_to_datetime(s, context='datetime'):
|
||||
mm = int(m.group(6))
|
||||
if (hh > 23 or mm > 59):
|
||||
raise ValueError("Bad timezone offset (%s)" % s)
|
||||
offset_seconds = hh*3600 + mm*60
|
||||
offset_seconds = hh * 3600 + mm * 60
|
||||
if (m.group(4) == '-'):
|
||||
offset_seconds = -offset_seconds
|
||||
# timetuple() ignores timezone information so we have to add in
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
"""Provide capture_stdout as contect manager."""
|
||||
|
||||
import sys, contextlib
|
||||
try: #python2
|
||||
import sys
|
||||
import contextlib
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
|
||||
# From http://stackoverflow.com/questions/2654834/capturing-stdout-within-the-same-process-in-python
|
||||
# From
|
||||
# http://stackoverflow.com/questions/2654834/capturing-stdout-within-the-same-process-in-python
|
||||
|
||||
|
||||
class Data(object):
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def capture_stdout():
|
||||
old = sys.stdout
|
||||
|
||||
@ -6,56 +6,61 @@ from resync.resource_set import ResourceSetDupeError
|
||||
from resync.change_list import ChangeList
|
||||
from resync.capability_list import CapabilityList
|
||||
|
||||
|
||||
class TestCapabilityList(unittest.TestCase):
|
||||
|
||||
def test01_add(self):
|
||||
# one
|
||||
caps = CapabilityList()
|
||||
r1 = Resource( uri='http://example.org/r1' )
|
||||
caps.add( r1 )
|
||||
self.assertEqual( len(caps), 1 )
|
||||
r1 = Resource(uri='http://example.org/r1')
|
||||
caps.add(r1)
|
||||
self.assertEqual(len(caps), 1)
|
||||
# dupe
|
||||
self.assertRaises( ResourceSetDupeError, caps.add, r1 )
|
||||
self.assertEqual( len(caps), 1 )
|
||||
self.assertRaises(ResourceSetDupeError, caps.add, r1)
|
||||
self.assertEqual(len(caps), 1)
|
||||
# dupe with replace
|
||||
caps = CapabilityList()
|
||||
caps.add( [r1,r1], replace=True )
|
||||
self.assertEqual( len(caps), 1 )
|
||||
caps.add([r1, r1], replace=True)
|
||||
self.assertEqual(len(caps), 1)
|
||||
# diff
|
||||
caps = CapabilityList()
|
||||
r2 = ChangeList( uri='http://example.org/r2' )
|
||||
caps.add( [r1,r2] )
|
||||
self.assertEqual( len(caps), 2 )
|
||||
|
||||
r2 = ChangeList(uri='http://example.org/r2')
|
||||
caps.add([r1, r2])
|
||||
self.assertEqual(len(caps), 2)
|
||||
|
||||
def test02_resourcelist(self):
|
||||
rl = ResourceList()
|
||||
caps = CapabilityList()
|
||||
caps.add_capability( rl, "http://example.org/resourcelist.xml" )
|
||||
caps.add_capability(rl, "http://example.org/resourcelist.xml")
|
||||
caps.md['from'] = "2013-02-07T22:39:00"
|
||||
self.assertEqual( len(caps), 1 )
|
||||
self.assertEqual( caps.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="capabilitylist" from="2013-02-07T22:39:00" /><url><loc>http://example.org/resourcelist.xml</loc><rs:md capability="resourcelist" /></url></urlset>' )
|
||||
self.assertEqual(len(caps), 1)
|
||||
self.assertEqual(caps.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="capabilitylist" from="2013-02-07T22:39:00" /><url><loc>http://example.org/resourcelist.xml</loc><rs:md capability="resourcelist" /></url></urlset>')
|
||||
|
||||
def test03_multiple(self):
|
||||
caps = CapabilityList()
|
||||
rl = ResourceList()
|
||||
caps.add_capability( rl, "rl.xml" )
|
||||
caps.add_capability(rl, "rl.xml")
|
||||
cl = ChangeList()
|
||||
caps.add_capability( cl, "cl.xml" )
|
||||
self.assertEqual( len(caps), 2 )
|
||||
caps.add_capability(cl, "cl.xml")
|
||||
self.assertEqual(len(caps), 2)
|
||||
xml = caps.as_xml()
|
||||
self.assertTrue( re.search( r'<loc>rl.xml</loc><rs:md capability="resourcelist" />', xml ) )
|
||||
self.assertTrue( re.search( r'<loc>cl.xml</loc><rs:md capability="changelist" />', xml) )
|
||||
self.assertTrue(
|
||||
re.search(r'<loc>rl.xml</loc><rs:md capability="resourcelist" />', xml))
|
||||
self.assertTrue(
|
||||
re.search(r'<loc>cl.xml</loc><rs:md capability="changelist" />', xml))
|
||||
|
||||
def test04_parse(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="capabilitylist" from="2013-02-07T22:39:00" /><url><loc>http://example.org/resourcelist.xml</loc><rs:md capability="resourcelist" /></url></urlset>'
|
||||
cl=CapabilityList()
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="capabilitylist" from="2013-02-07T22:39:00" /><url><loc>http://example.org/resourcelist.xml</loc><rs:md capability="resourcelist" /></url></urlset>'
|
||||
cl = CapabilityList()
|
||||
cl.parse(str_data=xml)
|
||||
self.assertEqual( cl.capability, 'capabilitylist')
|
||||
self.assertEqual( len(cl.resources), 1, 'got 1 resource')
|
||||
self.assertEqual(cl.capability, 'capabilitylist')
|
||||
self.assertEqual(len(cl.resources), 1, 'got 1 resource')
|
||||
[r] = cl.resources
|
||||
self.assertEqual( r.uri, 'http://example.org/resourcelist.xml', 'resourcelist uri')
|
||||
self.assertEqual( r.capability, 'resourcelist')
|
||||
self.assertEqual(
|
||||
r.uri, 'http://example.org/resourcelist.xml', 'resourcelist uri')
|
||||
self.assertEqual(r.capability, 'resourcelist')
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestCapabilityList)
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(
|
||||
TestCapabilityList)
|
||||
unittest.TextTestRunner().run(suite)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
import re
|
||||
|
||||
@ -11,53 +11,56 @@ from resync.resource import Resource
|
||||
from resync.change_dump import ChangeDump
|
||||
from resync.sitemap import SitemapParseError
|
||||
|
||||
|
||||
class TestChangeDump(unittest.TestCase):
|
||||
|
||||
def test01_as_xml(self):
|
||||
rd = ChangeDump()
|
||||
rd.add( Resource('a.zip',timestamp=1) )
|
||||
rd.add( Resource('b.zip',timestamp=2) )
|
||||
rd.add(Resource('a.zip', timestamp=1))
|
||||
rd.add(Resource('b.zip', timestamp=2))
|
||||
xml = rd.as_xml()
|
||||
#print(xml)
|
||||
self.assertTrue( re.search(r'<rs:md .*capability="changedump"', xml), 'XML has capability' )
|
||||
self.assertTrue( re.search(r'<url><loc>a.zip</loc><lastmod>1970-01-01T00:00:01Z</lastmod></url>', xml), 'XML has resource a' )
|
||||
# print(xml)
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md .*capability="changedump"', xml), 'XML has capability')
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>a.zip</loc><lastmod>1970-01-01T00:00:01Z</lastmod></url>', xml), 'XML has resource a')
|
||||
|
||||
def test10_parse(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="changedump" from="2013-01-01"/>\
|
||||
<url><loc>http://example.com/a.zip</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="12345" /></url>\
|
||||
<url><loc>http://example.com/b.zip</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="56789" /></url>\
|
||||
</urlset>'
|
||||
rd=ChangeDump()
|
||||
rd = ChangeDump()
|
||||
rd.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(rd.resources), 2, 'got 2 resource dumps')
|
||||
self.assertEqual( rd.md['capability'], 'changedump', 'capability set' )
|
||||
self.assertEqual( rd.md_from, '2013-01-01' )
|
||||
self.assertTrue( 'http://example.com/a.zip' in rd.resources )
|
||||
self.assertTrue( rd.resources['http://example.com/a.zip'].length, 12345 )
|
||||
self.assertTrue( 'http://example.com/b.zip' in rd.resources )
|
||||
self.assertTrue( rd.resources['http://example.com/b.zip'].length, 56789 )
|
||||
self.assertEqual(len(rd.resources), 2, 'got 2 resource dumps')
|
||||
self.assertEqual(rd.md['capability'], 'changedump', 'capability set')
|
||||
self.assertEqual(rd.md_from, '2013-01-01')
|
||||
self.assertTrue('http://example.com/a.zip' in rd.resources)
|
||||
self.assertTrue(rd.resources['http://example.com/a.zip'].length, 12345)
|
||||
self.assertTrue('http://example.com/b.zip' in rd.resources)
|
||||
self.assertTrue(rd.resources['http://example.com/b.zip'].length, 56789)
|
||||
|
||||
def test11_parse_no_capability(self):
|
||||
# For a resource dump this should be an error
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md at="2013-01-01"/>\
|
||||
<url><loc>http://example.com/a.zip</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="12" /></url>\
|
||||
</urlset>'
|
||||
rd=ChangeDump()
|
||||
self.assertRaises( SitemapParseError, rd.parse, fh=io.StringIO(xml) )
|
||||
rd = ChangeDump()
|
||||
self.assertRaises(SitemapParseError, rd.parse, fh=io.StringIO(xml))
|
||||
|
||||
def test12_parse_bad_capability(self):
|
||||
# the <rs:md capability="bad_capability".. should give error
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="bad_capability" from="2013-01-01"/>\
|
||||
<url><loc>http://example.com/bad_res_1</loc><lastmod>2012-03-14T18:37:36Z</lastmod></url>\
|
||||
</urlset>'
|
||||
rd=ChangeDump()
|
||||
self.assertRaises( SitemapParseError, rd.parse, fh=io.StringIO(xml) )
|
||||
rd = ChangeDump()
|
||||
self.assertRaises(SitemapParseError, rd.parse, fh=io.StringIO(xml))
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestChangeDump)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
import re
|
||||
|
||||
@ -12,86 +12,90 @@ from resync.change_list import ChangeList, ChangeTypeError
|
||||
from resync.resource_list import ResourceList
|
||||
from resync.sitemap import SitemapParseError
|
||||
|
||||
|
||||
class TestChangeList(unittest.TestCase):
|
||||
|
||||
def test01_add_if_changed(self):
|
||||
cl = ChangeList()
|
||||
cl.add_if_changed( Resource('a',timestamp=1,change='updated') )
|
||||
self.assertEqual( len(cl), 1 )
|
||||
self.assertRaises( ChangeTypeError, cl.add_if_changed,
|
||||
Resource('c',timestamp=3) )
|
||||
cl.add_if_changed(Resource('a', timestamp=1, change='updated'))
|
||||
self.assertEqual(len(cl), 1)
|
||||
self.assertRaises(ChangeTypeError, cl.add_if_changed,
|
||||
Resource('c', timestamp=3))
|
||||
|
||||
def test02_set_with_repeats(self):
|
||||
cl = ChangeList()
|
||||
cl.add( Resource('a',timestamp=1,change='updated') )
|
||||
cl.add( Resource('b',timestamp=1,change='created') )
|
||||
cl.add( Resource('c',timestamp=1,change='deleted') )
|
||||
cl.add( Resource('a',timestamp=2,change='deleted') )
|
||||
cl.add( Resource('b',timestamp=2,change='updated') )
|
||||
cl.add(Resource('a', timestamp=1, change='updated'))
|
||||
cl.add(Resource('b', timestamp=1, change='created'))
|
||||
cl.add(Resource('c', timestamp=1, change='deleted'))
|
||||
cl.add(Resource('a', timestamp=2, change='deleted'))
|
||||
cl.add(Resource('b', timestamp=2, change='updated'))
|
||||
self.assertEqual(len(cl), 5, "5 changes in change_list")
|
||||
|
||||
def test03_with_repeats_again(self):
|
||||
r1 = Resource(uri='a',length=1,change='created')
|
||||
r2 = Resource(uri='b',length=2,change='created')
|
||||
r1 = Resource(uri='a', length=1, change='created')
|
||||
r2 = Resource(uri='b', length=2, change='created')
|
||||
i = ChangeList()
|
||||
i.add(r1)
|
||||
i.add(r2)
|
||||
self.assertEqual( len(i), 2 )
|
||||
self.assertEqual(len(i), 2)
|
||||
# Can add another Resource with same URI
|
||||
r1d = Resource(uri='a',length=10,change='created')
|
||||
r1d = Resource(uri='a', length=10, change='created')
|
||||
i.add(r1d)
|
||||
self.assertEqual( len(i), 3 )
|
||||
self.assertEqual(len(i), 3)
|
||||
|
||||
def test04_change_list(self):
|
||||
cl = ChangeList()
|
||||
cl.add( Resource('a',timestamp=1,change='created') )
|
||||
cl.add( Resource('b',timestamp=2,change='created') )
|
||||
cl.add( Resource('c',timestamp=3,change='created') )
|
||||
cl.add( Resource('d',timestamp=4,change='created') )
|
||||
cl.add( Resource('e',timestamp=5,change='created') )
|
||||
cl.add(Resource('a', timestamp=1, change='created'))
|
||||
cl.add(Resource('b', timestamp=2, change='created'))
|
||||
cl.add(Resource('c', timestamp=3, change='created'))
|
||||
cl.add(Resource('d', timestamp=4, change='created'))
|
||||
cl.add(Resource('e', timestamp=5, change='created'))
|
||||
self.assertEqual(len(cl), 5, "5 things in src")
|
||||
|
||||
def test05_iter(self):
|
||||
i = ChangeList()
|
||||
i.add( Resource('a',timestamp=1,change='created') )
|
||||
i.add( Resource('b',timestamp=2,change='created') )
|
||||
i.add( Resource('c',timestamp=3,change='created') )
|
||||
i.add( Resource('d',timestamp=4,change='created') )
|
||||
resources=[]
|
||||
i.add(Resource('a', timestamp=1, change='created'))
|
||||
i.add(Resource('b', timestamp=2, change='created'))
|
||||
i.add(Resource('c', timestamp=3, change='created'))
|
||||
i.add(Resource('d', timestamp=4, change='created'))
|
||||
resources = []
|
||||
for r in i:
|
||||
resources.append(r)
|
||||
self.assertEqual(len(resources), 4)
|
||||
self.assertEqual( resources[0].uri, 'a')
|
||||
self.assertEqual( resources[3].uri, 'd')
|
||||
self.assertEqual(resources[0].uri, 'a')
|
||||
self.assertEqual(resources[3].uri, 'd')
|
||||
|
||||
def test06_add_changed_resources(self):
|
||||
added = ResourceList()
|
||||
added.add( Resource('a',timestamp=1,change='created') )
|
||||
added.add( Resource('d',timestamp=4,change='created') )
|
||||
added.add(Resource('a', timestamp=1, change='created'))
|
||||
added.add(Resource('d', timestamp=4, change='created'))
|
||||
self.assertEqual(len(added), 2, "2 things in added resource_list")
|
||||
changes = ChangeList()
|
||||
changes.add_changed_resources( added, change='created' )
|
||||
changes.add_changed_resources(added, change='created')
|
||||
self.assertEqual(len(changes), 2, "2 things added")
|
||||
i = iter(changes)
|
||||
first = next(i)
|
||||
self.assertEqual(first.uri, 'a', "changes[0].uri=a")
|
||||
self.assertEqual(first.timestamp, 1, "changes[0].timestamp=1")
|
||||
self.assertEqual(first.change, 'created') #, "changes[0].change=createdd")
|
||||
# , "changes[0].change=createdd")
|
||||
self.assertEqual(first.change, 'created')
|
||||
second = next(i)
|
||||
self.assertEqual(second.timestamp, 4, "changes[1].timestamp=4")
|
||||
self.assertEqual(second.change, 'created', "changes[1].change=createdd")
|
||||
self.assertEqual(second.change, 'created',
|
||||
"changes[1].change=createdd")
|
||||
# Now add some with updated (one same, one diff)
|
||||
updated = ResourceList()
|
||||
updated.add( Resource('a',timestamp=5,change='created') )
|
||||
updated.add( Resource('b',timestamp=6,change='created') )
|
||||
updated.add(Resource('a', timestamp=5, change='created'))
|
||||
updated.add(Resource('b', timestamp=6, change='created'))
|
||||
self.assertEqual(len(updated), 2, "2 things in updated resource_list")
|
||||
changes.add_changed_resources( updated, change='updated' )
|
||||
changes.add_changed_resources(updated, change='updated')
|
||||
self.assertEqual(len(changes), 4, "4 = 2 old + 2 things updated")
|
||||
# Make new resource_list from the changes which should not have dupes
|
||||
dst = ResourceList()
|
||||
dst.add( changes, replace=True )
|
||||
dst.add(changes, replace=True)
|
||||
self.assertEqual(len(dst), 3, "3 unique resources")
|
||||
self.assertEqual(dst.resources['a'].timestamp, 5 ) # 5 was later in last the 1
|
||||
# 5 was later in last the 1
|
||||
self.assertEqual(dst.resources['a'].timestamp, 5)
|
||||
self.assertEqual(dst.resources['a'].change, 'updated')
|
||||
self.assertEqual(dst.resources['b'].timestamp, 6)
|
||||
self.assertEqual(dst.resources['b'].change, 'updated')
|
||||
@ -101,44 +105,47 @@ class TestChangeList(unittest.TestCase):
|
||||
def test07_as_xml(self):
|
||||
cl = ChangeList()
|
||||
cl.md_from = '1970-01-01T00:00:00Z'
|
||||
cl.add( Resource('a',timestamp=1,change='updated') )
|
||||
cl.add( Resource('b',timestamp=2,change='updated') )
|
||||
cl.add(Resource('a', timestamp=1, change='updated'))
|
||||
cl.add(Resource('b', timestamp=2, change='updated'))
|
||||
xml = cl.as_xml()
|
||||
self.assertTrue( re.search(r'<rs:md .*capability="changelist"', xml), 'XML has capability' )
|
||||
self.assertTrue( re.search(r'<rs:md .*from="\d\d\d\d\-\d\d\-\d\dT\d\d:\d\d:\d\dZ"', xml), 'XML has from to seconds precision (and not more)' )
|
||||
self.assertTrue( re.search(r'<url><loc>a</loc><lastmod>1970-01-01T00:00:01Z</lastmod>', xml), 'XML has resource a' )
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md .*capability="changelist"', xml), 'XML has capability')
|
||||
self.assertTrue(re.search(r'<rs:md .*from="\d\d\d\d\-\d\d\-\d\dT\d\d:\d\d:\d\dZ"',
|
||||
xml), 'XML has from to seconds precision (and not more)')
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>a</loc><lastmod>1970-01-01T00:00:01Z</lastmod>', xml), 'XML has resource a')
|
||||
|
||||
def test08_parse(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="changelist" from="2013-01-01"/>\
|
||||
<url><loc>/tmp/rs_test/src/file_a</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md change="updated" length="12" /></url>\
|
||||
<url><loc>/tmp/rs_test/src/file_b</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md change="deleted" length="32" /></url>\
|
||||
</urlset>'
|
||||
cl=ChangeList()
|
||||
cl = ChangeList()
|
||||
cl.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(cl.resources), 2, 'got 2 resources')
|
||||
self.assertEqual( cl.md['capability'], 'changelist', 'capability set' )
|
||||
self.assertEqual( cl.md['md_from'], '2013-01-01' )
|
||||
self.assertEqual(len(cl.resources), 2, 'got 2 resources')
|
||||
self.assertEqual(cl.md['capability'], 'changelist', 'capability set')
|
||||
self.assertEqual(cl.md['md_from'], '2013-01-01')
|
||||
|
||||
def test09_parse_no_capability(self):
|
||||
# missing capability is an error for changelist
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<url><loc>http://example.com/res1</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md change="updated"/></url>\
|
||||
</urlset>'
|
||||
cl=ChangeList()
|
||||
self.assertRaises( SitemapParseError, cl.parse, fh=io.StringIO(xml) )
|
||||
cl = ChangeList()
|
||||
self.assertRaises(SitemapParseError, cl.parse, fh=io.StringIO(xml))
|
||||
|
||||
def test10_parse_bad_capability(self):
|
||||
# the <rs:md capability="bad_capability".. should give error
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="bad_capability" from="2013-01-01"/>\
|
||||
<url><loc>http://example.com/bad_res_1</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md change="updated"/></url>\
|
||||
</urlset>'
|
||||
cl=ChangeList()
|
||||
self.assertRaises( SitemapParseError, cl.parse, fh=io.StringIO(xml) )
|
||||
cl = ChangeList()
|
||||
self.assertRaises(SitemapParseError, cl.parse, fh=io.StringIO(xml))
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestChangeList)
|
||||
|
||||
@ -15,323 +15,366 @@ from resync.change_list import ChangeList
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class TestClient(TestCase):
|
||||
|
||||
def test01_make_resource_list_empty(self):
|
||||
c = Client()
|
||||
# No mapping is an error
|
||||
self.assertRaises( ClientFatalError, c.build_resource_list )
|
||||
self.assertRaises(ClientFatalError, c.build_resource_list)
|
||||
|
||||
def test02_bad_source_uri(self):
|
||||
c = Client()
|
||||
self.assertRaises( ClientFatalError, c.baseline_or_audit )
|
||||
c.set_mappings( ['http://example.org/bbb','/tmp/this_does_not_exist'] )
|
||||
self.assertRaises( ClientFatalError, c.baseline_or_audit )
|
||||
self.assertRaises(ClientFatalError, c.baseline_or_audit)
|
||||
c.set_mappings(['http://example.org/bbb', '/tmp/this_does_not_exist'])
|
||||
self.assertRaises(ClientFatalError, c.baseline_or_audit)
|
||||
|
||||
def test03_sitemap_uri(self):
|
||||
c = Client()
|
||||
c.set_mappings( ['http://example.org/c','/tmp/not_there_at_all'] )
|
||||
self.assertEqual( c.sitemap_uri('abcd1'), 'http://example.org/c/abcd1' )
|
||||
self.assertEqual( c.sitemap_uri('/abcd2'), '/abcd2' )
|
||||
self.assertEqual( c.sitemap_uri('scheme:/abcd3'), 'scheme:/abcd3' )
|
||||
c.set_mappings(['http://example.org/c', '/tmp/not_there_at_all'])
|
||||
self.assertEqual(c.sitemap_uri('abcd1'), 'http://example.org/c/abcd1')
|
||||
self.assertEqual(c.sitemap_uri('/abcd2'), '/abcd2')
|
||||
self.assertEqual(c.sitemap_uri('scheme:/abcd3'), 'scheme:/abcd3')
|
||||
|
||||
def test10_baseline_or_audit(self):
|
||||
# FIXME - this is the guts of the client, tough to test, need to work through more cases...
|
||||
# FIXME - this is the guts of the client, tough to test, need to work
|
||||
# through more cases...
|
||||
c = Client()
|
||||
dst = os.path.join(self.tmpdir,'dst_dir1')
|
||||
c.set_mappings( ['file:tests/testdata/client/dir1',dst] )
|
||||
dst = os.path.join(self.tmpdir, 'dst_dir1')
|
||||
c.set_mappings(['file:tests/testdata/client/dir1', dst])
|
||||
# audit with empty dst, should say 3 to create
|
||||
with LogCapture() as lc:
|
||||
c.baseline_or_audit(audit_only=True)
|
||||
self.assertTrue( re.match(r'Status:\s+NOT IN SYNC.*to create=3', lc.records[-2].msg) )
|
||||
self.assertEqual( lc.records[-1].msg, 'Completed audit' )
|
||||
self.assertTrue(
|
||||
re.match(r'Status:\s+NOT IN SYNC.*to create=3', lc.records[-2].msg))
|
||||
self.assertEqual(lc.records[-1].msg, 'Completed audit')
|
||||
# now do the sync
|
||||
with LogCapture() as lc:
|
||||
c.baseline_or_audit()
|
||||
self.assertTrue( re.match(r'Status:\s+SYNCED.*created=3', lc.records[-2].msg) )
|
||||
self.assertEqual( lc.records[-1].msg, 'Completed baseline sync' )
|
||||
self.assertTrue(
|
||||
re.match(r'Status:\s+SYNCED.*created=3', lc.records[-2].msg))
|
||||
self.assertEqual(lc.records[-1].msg, 'Completed baseline sync')
|
||||
|
||||
def test18_update_resource(self):
|
||||
c = Client()
|
||||
resource = Resource(uri='http://example.org/dir/2')
|
||||
filename = os.path.join(self.tmpdir,'dir/resource2')
|
||||
filename = os.path.join(self.tmpdir, 'dir/resource2')
|
||||
# dryrun
|
||||
with LogCapture() as lc:
|
||||
c.dryrun = True
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.update_resource( resource, filename )
|
||||
self.assertEqual( n, 0 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('dryrun: would GET http://example.org/dir/2 ') )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.update_resource(resource, filename)
|
||||
self.assertEqual(n, 0)
|
||||
self.assertTrue(
|
||||
lc.records[-1].msg.startswith('dryrun: would GET http://example.org/dir/2 '))
|
||||
c.dryrun = False
|
||||
# get from file uri that does not exist
|
||||
resource = Resource(uri='file:tests/testdata/i_do_not_exist')
|
||||
self.assertRaises( ClientFatalError, c.update_resource, resource, filename )
|
||||
# get from file uri that does not exist but with c.ignore_failures to log
|
||||
self.assertRaises(ClientFatalError,
|
||||
c.update_resource, resource, filename)
|
||||
# get from file uri that does not exist but with c.ignore_failures to
|
||||
# log
|
||||
resource = Resource(uri='file:tests/testdata/i_do_not_exist')
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
c.ignore_failures = True
|
||||
n = c.update_resource( resource, filename )
|
||||
self.assertEqual( n, 0 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('Failed to GET file:tests/testdata/i_do_not_exist ') )
|
||||
n = c.update_resource(resource, filename)
|
||||
self.assertEqual(n, 0)
|
||||
self.assertTrue(
|
||||
lc.records[-1].msg.startswith('Failed to GET file:tests/testdata/i_do_not_exist '))
|
||||
# get from file uri
|
||||
resource = Resource(uri='file:tests/testdata/examples_from_spec/resourcesync_ex_1.xml',
|
||||
length=355, md5='abc',
|
||||
timestamp=10)
|
||||
c.last_timestamp = 0
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.update_resource( resource, filename )
|
||||
self.assertEqual( n, 1 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('Event: {') )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.update_resource(resource, filename)
|
||||
self.assertEqual(n, 1)
|
||||
self.assertTrue(lc.records[-1].msg.startswith('Event: {'))
|
||||
# get from file uri with length and md5 warnings
|
||||
resource = Resource(uri='file:tests/testdata/examples_from_spec/resourcesync_ex_1.xml',
|
||||
length=111, md5='abc',
|
||||
timestamp=10)
|
||||
c.last_timestamp = 0
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
c.checksum = True
|
||||
n = c.update_resource( resource, filename )
|
||||
self.assertEqual( n, 1 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('MD5 mismatch ') )
|
||||
self.assertTrue( lc.records[-2].msg.startswith('Downloaded size for ') )
|
||||
self.assertTrue( lc.records[-3].msg.startswith('Event: {') )
|
||||
n = c.update_resource(resource, filename)
|
||||
self.assertEqual(n, 1)
|
||||
self.assertTrue(lc.records[-1].msg.startswith('MD5 mismatch '))
|
||||
self.assertTrue(
|
||||
lc.records[-2].msg.startswith('Downloaded size for '))
|
||||
self.assertTrue(lc.records[-3].msg.startswith('Event: {'))
|
||||
|
||||
def test19_delete_resource(self):
|
||||
c = Client()
|
||||
resource = Resource(uri='http://example.org/1')
|
||||
filename = os.path.join(self.tmpdir,'resource1')
|
||||
filename = os.path.join(self.tmpdir, 'resource1')
|
||||
c.last_timestamp = 5
|
||||
# no delete, no timestamp update
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource( resource, filename )
|
||||
self.assertEqual( n, 0 )
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'nodelete: would delete http://example.org/1 (--delete to enable)' )
|
||||
self.assertEqual( c.last_timestamp, 5 )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource(resource, filename)
|
||||
self.assertEqual(n, 0)
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'nodelete: would delete http://example.org/1 (--delete to enable)')
|
||||
self.assertEqual(c.last_timestamp, 5)
|
||||
# no delete but timestamp update
|
||||
resource.timestamp = 10
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource( resource, filename )
|
||||
self.assertEqual( n, 0 )
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'nodelete: would delete http://example.org/1 (--delete to enable)' )
|
||||
self.assertEqual( c.last_timestamp, 10 )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource(resource, filename)
|
||||
self.assertEqual(n, 0)
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'nodelete: would delete http://example.org/1 (--delete to enable)')
|
||||
self.assertEqual(c.last_timestamp, 10)
|
||||
# allow delete but dryrun
|
||||
with LogCapture() as lc:
|
||||
c.dryrun = True
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource( resource, filename, allow_deletion=True )
|
||||
self.assertEqual( n, 0 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('dryrun: would delete http://example.org/1') )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource(resource, filename, allow_deletion=True)
|
||||
self.assertEqual(n, 0)
|
||||
self.assertTrue(
|
||||
lc.records[-1].msg.startswith('dryrun: would delete http://example.org/1'))
|
||||
c.dryrun = False
|
||||
# allow delete but no resource present
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource( resource, filename, allow_deletion=True )
|
||||
self.assertEqual( n, 0 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('Failed to DELETE http://example.org/1') )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource(resource, filename, allow_deletion=True)
|
||||
self.assertEqual(n, 0)
|
||||
self.assertTrue(
|
||||
lc.records[-1].msg.startswith('Failed to DELETE http://example.org/1'))
|
||||
# successful deletion, first make file...
|
||||
with open(filename, 'w') as fh:
|
||||
fh.write('delete me')
|
||||
fh.close()
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource( resource, filename, allow_deletion=True )
|
||||
self.assertEqual( n, 1 )
|
||||
self.assertTrue( lc.records[-1].msg.startswith('Event: {') )
|
||||
self.assertTrue( lc.records[-2].msg.startswith('deleted: http://example.org/1 ->') )
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
n = c.delete_resource(resource, filename, allow_deletion=True)
|
||||
self.assertEqual(n, 1)
|
||||
self.assertTrue(lc.records[-1].msg.startswith('Event: {'))
|
||||
self.assertTrue(
|
||||
lc.records[-2].msg.startswith('deleted: http://example.org/1 ->'))
|
||||
|
||||
def test20_parse_document(self):
|
||||
# Key property of the parse_document() method is that it parses the
|
||||
# document and identifies its type
|
||||
c = Client()
|
||||
with capture_stdout() as capturer:
|
||||
c.sitemap_name='tests/testdata/examples_from_spec/resourcesync_ex_1.xml'
|
||||
c.sitemap_name = 'tests/testdata/examples_from_spec/resourcesync_ex_1.xml'
|
||||
c.parse_document()
|
||||
self.assertTrue( re.search(r'Parsed resourcelist document with 2 entries',capturer.result) )
|
||||
self.assertTrue(
|
||||
re.search(r'Parsed resourcelist document with 2 entries', capturer.result))
|
||||
with capture_stdout() as capturer:
|
||||
c.sitemap_name='tests/testdata/examples_from_spec/resourcesync_ex_17.xml'
|
||||
c.sitemap_name = 'tests/testdata/examples_from_spec/resourcesync_ex_17.xml'
|
||||
c.parse_document()
|
||||
self.assertTrue( re.search(r'Parsed resourcedump document with 3 entries',capturer.result) )
|
||||
self.assertTrue(
|
||||
re.search(r'Parsed resourcedump document with 3 entries', capturer.result))
|
||||
with capture_stdout() as capturer:
|
||||
c.sitemap_name='tests/testdata/examples_from_spec/resourcesync_ex_19.xml'
|
||||
c.sitemap_name = 'tests/testdata/examples_from_spec/resourcesync_ex_19.xml'
|
||||
c.parse_document()
|
||||
self.assertTrue( re.search(r'Parsed changelist document with 4 entries',capturer.result) )
|
||||
self.assertTrue(
|
||||
re.search(r'Parsed changelist document with 4 entries', capturer.result))
|
||||
with capture_stdout() as capturer:
|
||||
c.sitemap_name='tests/testdata/examples_from_spec/resourcesync_ex_22.xml'
|
||||
c.sitemap_name = 'tests/testdata/examples_from_spec/resourcesync_ex_22.xml'
|
||||
c.parse_document()
|
||||
self.assertTrue( re.search(r'Parsed changedump document with 3 entries',capturer.result) )
|
||||
self.assertTrue(
|
||||
re.search(r'Parsed changedump document with 3 entries', capturer.result))
|
||||
# Document that doesn't exist
|
||||
c.sitemap_name='/does_not_exist'
|
||||
self.assertRaises( ClientFatalError, c.parse_document )
|
||||
c.sitemap_name = '/does_not_exist'
|
||||
self.assertRaises(ClientFatalError, c.parse_document)
|
||||
# and verbose with truncation...
|
||||
with capture_stdout() as capturer:
|
||||
c.verbose = True
|
||||
c.sitemap_name = 'tests/testdata/examples_from_spec/resourcesync_ex_1.xml'
|
||||
c.max_sitemap_entries = 1
|
||||
c.max_sitemap_entries = 1
|
||||
c.parse_document()
|
||||
self.assertTrue( re.search(r'Showing first 1 entries', capturer.result ) )
|
||||
self.assertTrue( re.search(r'\[0\] ', capturer.result ) )
|
||||
self.assertFalse( re.search(r'\[1\] ', capturer.result ) )
|
||||
self.assertTrue(re.search(r'Showing first 1 entries', capturer.result))
|
||||
self.assertTrue(re.search(r'\[0\] ', capturer.result))
|
||||
self.assertFalse(re.search(r'\[1\] ', capturer.result))
|
||||
|
||||
def test40_write_resource_list_mappings(self):
|
||||
c = Client()
|
||||
c.set_mappings( ['http://example.org/','tests/testdata/'] )
|
||||
c.set_mappings(['http://example.org/', 'tests/testdata/'])
|
||||
# with no explicit paths seting the mapping will be used
|
||||
with capture_stdout() as capturer:
|
||||
c.write_resource_list()
|
||||
#sys.stderr.write(capturer.result)
|
||||
self.assertTrue( re.search(r'<rs:md at="\S+" capability="resourcelist"', capturer.result ) )
|
||||
self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_a</loc>', capturer.result ) )
|
||||
self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_b</loc>', capturer.result ) )
|
||||
self.assertTrue( re.search(r'<url><loc>http://example.org/dir2/file_x</loc>', capturer.result ) )
|
||||
# sys.stderr.write(capturer.result)
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md at="\S+" capability="resourcelist"', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>http://example.org/dir1/file_a</loc>', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>http://example.org/dir1/file_b</loc>', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>http://example.org/dir2/file_x</loc>', capturer.result))
|
||||
|
||||
def test41_write_resource_list_path(self):
|
||||
c = Client()
|
||||
c.set_mappings( ['http://example.org/','tests/testdata/'] )
|
||||
links=[{'rel':'uri_c','href':'uri_d'}]
|
||||
# with an explicit paths setting only the specified paths will be included
|
||||
c.set_mappings(['http://example.org/', 'tests/testdata/'])
|
||||
links = [{'rel': 'uri_c', 'href': 'uri_d'}]
|
||||
# with an explicit paths setting only the specified paths will be
|
||||
# included
|
||||
with capture_stdout() as capturer:
|
||||
c.write_resource_list(paths='tests/testdata/dir1', links=links)
|
||||
self.assertTrue( re.search(r'<rs:md at="\S+" capability="resourcelist"', capturer.result ) )
|
||||
self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_a</loc>', capturer.result ) )
|
||||
self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_b</loc>', capturer.result ) )
|
||||
self.assertFalse( re.search(r'<url><loc>http://example.org/dir2/file_x</loc>', capturer.result ) )
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md at="\S+" capability="resourcelist"', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>http://example.org/dir1/file_a</loc>', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>http://example.org/dir1/file_b</loc>', capturer.result))
|
||||
self.assertFalse(
|
||||
re.search(r'<url><loc>http://example.org/dir2/file_x</loc>', capturer.result))
|
||||
# check link present
|
||||
self.assertTrue( re.search(r'rel="uri_c"', capturer.result ) )
|
||||
self.assertTrue( re.search(r'href="uri_d"', capturer.result ) )
|
||||
self.assertTrue(re.search(r'rel="uri_c"', capturer.result))
|
||||
self.assertTrue(re.search(r'href="uri_d"', capturer.result))
|
||||
# Travis CI does not preserve timestamps from github so test here for the file
|
||||
# size but not the datestamp
|
||||
#self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_a</loc><lastmod>[\w\-:]+</lastmod><rs:md length="20" /></url>', capturer.result ) )
|
||||
#self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_b</loc><lastmod>[\w\-:]+</lastmod><rs:md length="45" /></url>', capturer.result ) )
|
||||
# to file
|
||||
outfile = os.path.join(self.tmpdir,'rl_out.xml')
|
||||
# self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_a</loc><lastmod>[\w\-:]+</lastmod><rs:md length="20" /></url>', capturer.result ) )
|
||||
# self.assertTrue( re.search(r'<url><loc>http://example.org/dir1/file_b</loc><lastmod>[\w\-:]+</lastmod><rs:md length="45" /></url>', capturer.result ) )
|
||||
# to file
|
||||
outfile = os.path.join(self.tmpdir, 'rl_out.xml')
|
||||
c.write_resource_list(paths='tests/testdata/dir1', outfile=outfile)
|
||||
self.assertTrue( os.path.getsize(outfile)>100 )
|
||||
self.assertTrue(os.path.getsize(outfile) > 100)
|
||||
# dump instead (default file)
|
||||
c.default_resource_dump = os.path.join(self.tmpdir,'rl_out_dump_def')
|
||||
outfile = c.default_resource_dump+'00000.zip'
|
||||
self.assertFalse( os.path.exists(outfile) )
|
||||
c.default_resource_dump = os.path.join(self.tmpdir, 'rl_out_dump_def')
|
||||
outfile = c.default_resource_dump + '00000.zip'
|
||||
self.assertFalse(os.path.exists(outfile))
|
||||
c.write_resource_list(paths='tests/testdata/dir1', dump=True)
|
||||
self.assertTrue( os.path.getsize(outfile)>100 )
|
||||
self.assertTrue(os.path.getsize(outfile) > 100)
|
||||
# (specific file)
|
||||
outbase = os.path.join(self.tmpdir,'rl_out_dump')
|
||||
outfile = outbase+'00000.zip'
|
||||
self.assertFalse( os.path.exists(outfile) )
|
||||
c.write_resource_list(paths='tests/testdata/dir1', dump=True, outfile=outbase)
|
||||
self.assertTrue( os.path.getsize(outfile)>100 )
|
||||
outbase = os.path.join(self.tmpdir, 'rl_out_dump')
|
||||
outfile = outbase + '00000.zip'
|
||||
self.assertFalse(os.path.exists(outfile))
|
||||
c.write_resource_list(paths='tests/testdata/dir1',
|
||||
dump=True, outfile=outbase)
|
||||
self.assertTrue(os.path.getsize(outfile) > 100)
|
||||
|
||||
def test45_write_change_list(self):
|
||||
c = Client()
|
||||
ex1 = 'tests/testdata/examples_from_spec/resourcesync_ex_1.xml'
|
||||
with capture_stdout() as capturer:
|
||||
c.write_change_list(ref_sitemap=ex1, newref_sitemap=ex1)
|
||||
self.assertTrue( re.search(r'<rs:md capability="changelist"', capturer.result) )
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md capability="changelist"', capturer.result))
|
||||
# compare ex1 with testdata on disk
|
||||
c.set_mappings( ['http://example.org/','tests/testdata/'] )
|
||||
c.set_mappings(['http://example.org/', 'tests/testdata/'])
|
||||
with capture_stdout() as capturer:
|
||||
c.write_change_list(ref_sitemap=ex1, paths='tests/testdata/dir1')
|
||||
self.assertTrue( re.search(r'<rs:md capability="changelist"', capturer.result) )
|
||||
self.assertTrue( re.search(r'<url><loc>http://example.com/res1</loc><rs:md change="deleted" /></url>', capturer.result) )
|
||||
# to file
|
||||
outfile = os.path.join(self.tmpdir,'cl_out.xml')
|
||||
c.write_change_list(ref_sitemap=ex1, newref_sitemap=ex1, outfile=outfile)
|
||||
self.assertTrue( os.path.getsize(outfile)>100 )
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md capability="changelist"', capturer.result))
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>http://example.com/res1</loc><rs:md change="deleted" /></url>', capturer.result))
|
||||
# to file
|
||||
outfile = os.path.join(self.tmpdir, 'cl_out.xml')
|
||||
c.write_change_list(
|
||||
ref_sitemap=ex1, newref_sitemap=ex1, outfile=outfile)
|
||||
self.assertTrue(os.path.getsize(outfile) > 100)
|
||||
|
||||
def test46_write_capability_list(self):
|
||||
c = Client()
|
||||
caps = { 'a':'uri_a', 'b':'uri_b' }
|
||||
caps = {'a': 'uri_a', 'b': 'uri_b'}
|
||||
# simple case to STDOUT
|
||||
with capture_stdout() as capturer:
|
||||
c.write_capability_list( caps )
|
||||
self.assertTrue( re.search(r'<urlset ',capturer.result) )
|
||||
self.assertTrue( re.search(r'<rs:md capability="capabilitylist" />',capturer.result) )
|
||||
self.assertTrue( re.search(r'<url><loc>uri_a</loc><rs:md capability="a"',capturer.result) )
|
||||
self.assertTrue( re.search(r'<url><loc>uri_b</loc><rs:md capability="b"',capturer.result) )
|
||||
c.write_capability_list(caps)
|
||||
self.assertTrue(re.search(r'<urlset ', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md capability="capabilitylist" />', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>uri_a</loc><rs:md capability="a"', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<url><loc>uri_b</loc><rs:md capability="b"', capturer.result))
|
||||
# to file (just check that something is written)
|
||||
outfile = os.path.join(self.tmpdir,'caps_out.xml')
|
||||
outfile = os.path.join(self.tmpdir, 'caps_out.xml')
|
||||
c.write_capability_list(capabilities=caps, outfile=outfile)
|
||||
self.assertTrue( os.path.getsize(outfile)>100 )
|
||||
self.assertTrue(os.path.getsize(outfile) > 100)
|
||||
|
||||
def test47_write_source_description(self):
|
||||
c = Client()
|
||||
# simple case to STDOUT
|
||||
with capture_stdout() as capturer:
|
||||
c.write_source_description( [ 'a','b','c' ] )
|
||||
self.assertTrue( re.search(r'<urlset ',capturer.result) )
|
||||
self.assertTrue( re.search(r'<rs:md capability="description" />',capturer.result) )
|
||||
self.assertTrue( re.search(r'<url><loc>a</loc><rs:md capability="capabilitylist" /></url>',capturer.result) )
|
||||
self.assertTrue( re.search(r'<url><loc>b</loc><rs:md capability="capabilitylist" /></url>',capturer.result) )
|
||||
c.write_source_description(['a', 'b', 'c'])
|
||||
self.assertTrue(re.search(r'<urlset ', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md capability="description" />', capturer.result))
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>a</loc><rs:md capability="capabilitylist" /></url>', capturer.result))
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>b</loc><rs:md capability="capabilitylist" /></url>', capturer.result))
|
||||
# more complex case to STDOUT
|
||||
with capture_stdout() as capturer:
|
||||
c.write_source_description(capability_lists=['http://a.b/'], links=[{'rel':'c','href':'d'}])
|
||||
self.assertTrue( re.search(r'http://a.b/', capturer.result ) )
|
||||
c.write_source_description(
|
||||
capability_lists=['http://a.b/'], links=[{'rel': 'c', 'href': 'd'}])
|
||||
self.assertTrue(re.search(r'http://a.b/', capturer.result))
|
||||
# to file (just check that something is written)
|
||||
outfile = os.path.join(self.tmpdir,'sd_out.xml')
|
||||
c.write_source_description(capability_lists=['http://a.b/'], outfile=outfile, links=[{'rel':'c','href':'d'}])
|
||||
self.assertTrue( os.path.getsize(outfile)>100 )
|
||||
outfile = os.path.join(self.tmpdir, 'sd_out.xml')
|
||||
c.write_source_description(capability_lists=[
|
||||
'http://a.b/'], outfile=outfile, links=[{'rel': 'c', 'href': 'd'}])
|
||||
self.assertTrue(os.path.getsize(outfile) > 100)
|
||||
|
||||
def test48_write_dump_if_requested(self):
|
||||
c = Client()
|
||||
# no dump file
|
||||
self.assertFalse( c.write_dump_if_requested( ChangeList(), None ) )
|
||||
self.assertFalse(c.write_dump_if_requested(ChangeList(), None))
|
||||
# with dump file
|
||||
with capture_stdout() as capturer:
|
||||
c.write_dump_if_requested(ChangeList(),'/tmp/a_file')
|
||||
self.assertTrue( re.search(r'FIXME', capturer.result) )
|
||||
c.write_dump_if_requested(ChangeList(), '/tmp/a_file')
|
||||
self.assertTrue(re.search(r'FIXME', capturer.result))
|
||||
|
||||
def test49_read_reference_resource_list(self):
|
||||
c = Client()
|
||||
with capture_stdout() as capturer:
|
||||
rl = c.read_reference_resource_list('tests/testdata/examples_from_spec/resourcesync_ex_1.xml')
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertEqual( '', capturer.result )
|
||||
rl = c.read_reference_resource_list(
|
||||
'tests/testdata/examples_from_spec/resourcesync_ex_1.xml')
|
||||
self.assertEqual(len(rl), 2)
|
||||
self.assertEqual('', capturer.result)
|
||||
c.verbose = True
|
||||
with capture_stdout() as capturer:
|
||||
rl = c.read_reference_resource_list('tests/testdata/examples_from_spec/resourcesync_ex_1.xml')
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertTrue( re.search(r'http://example.com/res2', capturer.result) )
|
||||
rl = c.read_reference_resource_list(
|
||||
'tests/testdata/examples_from_spec/resourcesync_ex_1.xml')
|
||||
self.assertEqual(len(rl), 2)
|
||||
self.assertTrue(re.search(r'http://example.com/res2', capturer.result))
|
||||
c.verbose = True
|
||||
c.max_sitemap_entries = 1
|
||||
with capture_stdout() as capturer:
|
||||
rl = c.read_reference_resource_list('tests/testdata/examples_from_spec/resourcesync_ex_1.xml')
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertTrue( re.search(r'http://example.com/res1', capturer.result) )
|
||||
self.assertTrue( re.search(r'Showing first 1 entries', capturer.result) )
|
||||
self.assertFalse( re.search(r'http://example.com/res2', capturer.result) )
|
||||
rl = c.read_reference_resource_list(
|
||||
'tests/testdata/examples_from_spec/resourcesync_ex_1.xml')
|
||||
self.assertEqual(len(rl), 2)
|
||||
self.assertTrue(re.search(r'http://example.com/res1', capturer.result))
|
||||
self.assertTrue(re.search(r'Showing first 1 entries', capturer.result))
|
||||
self.assertFalse(
|
||||
re.search(r'http://example.com/res2', capturer.result))
|
||||
|
||||
def test50_log_status(self):
|
||||
c = Client()
|
||||
with LogCapture() as lc:
|
||||
c.logger = logging.getLogger('resync.client')
|
||||
c.log_status()
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: IN SYNC (created=0, updated=0, deleted=0)' )
|
||||
c.log_status(created=123,updated=456,deleted=789)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: IN SYNC (created=123, updated=456, deleted=789)' )
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: IN SYNC (created=0, updated=0, deleted=0)')
|
||||
c.log_status(created=123, updated=456, deleted=789)
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: IN SYNC (created=123, updated=456, deleted=789)')
|
||||
c.log_status(incremental=True)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: NO CHANGES (created=0, updated=0, deleted=0)' )
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: NO CHANGES (created=0, updated=0, deleted=0)')
|
||||
c.log_status(audit=True)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: IN SYNC (to create=0, to update=0, to delete=0)' )
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: IN SYNC (to create=0, to update=0, to delete=0)')
|
||||
c.log_status(in_sync=False, audit=True)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: NOT IN SYNC (to create=0, to update=0, to delete=0)' )
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: NOT IN SYNC (to create=0, to update=0, to delete=0)')
|
||||
c.log_status(in_sync=False, audit=False, to_delete=1)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: PART SYNCED (created=0, updated=0, to delete (--delete)=1)' )
|
||||
c.log_status(in_sync=False, audit=False, to_delete=1, incremental=1)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: PART APPLIED (created=0, updated=0, to delete (--delete)=1)' )
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: PART SYNCED (created=0, updated=0, to delete (--delete)=1)')
|
||||
c.log_status(in_sync=False, audit=False,
|
||||
to_delete=1, incremental=1)
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: PART APPLIED (created=0, updated=0, to delete (--delete)=1)')
|
||||
c.log_status(in_sync=False)
|
||||
self.assertEqual( lc.records[-1].msg,
|
||||
'Status: SYNCED (created=0, updated=0, deleted=0)' )
|
||||
self.assertEqual(lc.records[-1].msg,
|
||||
'Status: SYNCED (created=0, updated=0, deleted=0)')
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestClient)
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import sys
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import BytesIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
from resync.resource import Resource
|
||||
from resync.resource_list import ResourceList
|
||||
from resync.capability_list import CapabilityList
|
||||
@ -16,49 +14,51 @@ from resync.sitemap import Sitemap, SitemapIndexError, SitemapParseError
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_resync(args):
|
||||
args.insert(0,'bin/resync')
|
||||
args.insert(0, 'bin/resync')
|
||||
proc = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
(out, err) = proc.communicate()
|
||||
return(out)
|
||||
|
||||
|
||||
class TestClientLinkOptions(unittest.TestCase):
|
||||
|
||||
def test01_no_links(self):
|
||||
xml = run_resync(['--resourcelist',
|
||||
'http://example.org/t','tests/testdata/dir1'])
|
||||
'http://example.org/t', 'tests/testdata/dir1'])
|
||||
rl = ResourceList()
|
||||
rl.parse(fh=io.BytesIO(xml))
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertEqual( rl.link('describedby'), None )
|
||||
self.assertEqual(len(rl), 2)
|
||||
self.assertEqual(rl.link('describedby'), None)
|
||||
|
||||
def test02_resource_list_links(self):
|
||||
xml = run_resync(['--resourcelist',
|
||||
'--describedby-link=a',
|
||||
'--sourcedescription-link=b', #will be ignored
|
||||
'--sourcedescription-link=b', # will be ignored
|
||||
'--capabilitylist-link=c',
|
||||
'http://example.org/t','tests/testdata/dir1'])
|
||||
'http://example.org/t', 'tests/testdata/dir1'])
|
||||
rl = ResourceList()
|
||||
rl.parse(fh=io.BytesIO(xml))
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertNotEqual( rl.link('describedby'), None )
|
||||
self.assertEqual( rl.link('describedby')['href'], 'a' )
|
||||
self.assertNotEqual( rl.link('up'), None )
|
||||
self.assertEqual( rl.link('up')['href'], 'c' )
|
||||
self.assertEqual(len(rl), 2)
|
||||
self.assertNotEqual(rl.link('describedby'), None)
|
||||
self.assertEqual(rl.link('describedby')['href'], 'a')
|
||||
self.assertNotEqual(rl.link('up'), None)
|
||||
self.assertEqual(rl.link('up')['href'], 'c')
|
||||
|
||||
def test03_capability_list_links(self):
|
||||
xml = run_resync(['--capabilitylist=resourcelist=rl,changedump=cd',
|
||||
'--describedby-link=a',
|
||||
'--sourcedescription-link=b',
|
||||
'--capabilitylist-link=c' ]) #will be ignored
|
||||
'--capabilitylist-link=c']) # will be ignored
|
||||
capl = CapabilityList()
|
||||
capl.parse(fh=io.BytesIO(xml))
|
||||
self.assertEqual( len(capl), 2 )
|
||||
self.assertNotEqual( capl.link('describedby'), None )
|
||||
self.assertEqual( capl.link('describedby')['href'], 'a' )
|
||||
self.assertNotEqual( capl.link('up'), None )
|
||||
self.assertEqual( capl.link('up')['href'], 'b' )
|
||||
self.assertEqual(len(capl), 2)
|
||||
self.assertNotEqual(capl.link('describedby'), None)
|
||||
self.assertEqual(capl.link('describedby')['href'], 'a')
|
||||
self.assertNotEqual(capl.link('up'), None)
|
||||
self.assertEqual(capl.link('up')['href'], 'b')
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestClientLinkOptions)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -3,22 +3,23 @@ import os.path
|
||||
import unittest
|
||||
from resync.client_state import ClientState
|
||||
|
||||
|
||||
class TestClientState(TestCase):
|
||||
|
||||
def test01_set_and_get(self):
|
||||
cs = ClientState()
|
||||
self.assertEqual( cs.status_file, '.resync-client-status.cfg')
|
||||
cs.status_file = os.path.join(self.tmpdir,cs.status_file)
|
||||
self.assertEqual(cs.status_file, '.resync-client-status.cfg')
|
||||
cs.status_file = os.path.join(self.tmpdir, cs.status_file)
|
||||
# Get state
|
||||
site = 'https://this.site/'
|
||||
self.assertEqual( cs.get_state(site), None )
|
||||
self.assertEqual(cs.get_state(site), None)
|
||||
cs.set_state(site, 123)
|
||||
self.assertEqual( cs.get_state(site), 123 )
|
||||
self.assertEqual(cs.get_state(site), 123)
|
||||
cs.set_state(site, 456)
|
||||
self.assertEqual( cs.get_state(site), 456 )
|
||||
self.assertEqual(cs.get_state(site), 456)
|
||||
cs.set_state(site)
|
||||
self.assertEqual( cs.get_state(site), None )
|
||||
|
||||
self.assertEqual(cs.get_state(site), None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestClientUtils)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -4,71 +4,77 @@ import logging
|
||||
import os.path
|
||||
import re
|
||||
import unittest
|
||||
from resync.client_utils import init_logging,count_true_args,parse_links,parse_link,parse_capabilities,parse_capability_lists
|
||||
from resync.client_utils import init_logging, count_true_args, parse_links, parse_link, parse_capabilities, parse_capability_lists
|
||||
from resync.client import ClientFatalError
|
||||
|
||||
|
||||
class TestClientUtils(TestCase):
|
||||
|
||||
def test01_init_logging(self):
|
||||
#to_file=False, logfile=None, default_logfile='/tmp/resync.log',
|
||||
# human=True, verbose=False, eval_mode=False,
|
||||
# to_file=False, logfile=None, default_logfile='/tmp/resync.log',
|
||||
# human=True, verbose=False, eval_mode=False,
|
||||
# default_logger='client', extra_loggers=None):
|
||||
tmplog = os.path.join(self.tmpdir,'tmp.log')
|
||||
init_logging( to_file=True, default_logfile=tmplog, extra_loggers=['x1','x2'] )
|
||||
tmplog = os.path.join(self.tmpdir, 'tmp.log')
|
||||
init_logging(to_file=True, default_logfile=tmplog,
|
||||
extra_loggers=['x1', 'x2'])
|
||||
# check x1 and x2 set, not x3 (can tell by level)
|
||||
self.assertTrue( logging.getLogger('x1').level, logging.DEBUG )
|
||||
self.assertTrue( logging.getLogger('x2').level, logging.DEBUG )
|
||||
self.assertEqual( logging.getLogger('x3').level, 0 )
|
||||
self.assertTrue(logging.getLogger('x1').level, logging.DEBUG)
|
||||
self.assertTrue(logging.getLogger('x2').level, logging.DEBUG)
|
||||
self.assertEqual(logging.getLogger('x3').level, 0)
|
||||
# write something, check goes to file
|
||||
log = logging.getLogger('resync')
|
||||
log.warning('PIGS MIGHT FLY')
|
||||
logtxt = open(tmplog,'r').read()
|
||||
self.assertTrue( re.search(r'WARNING \| PIGS MIGHT FLY', logtxt) )
|
||||
logtxt = open(tmplog, 'r').read()
|
||||
self.assertTrue(re.search(r'WARNING \| PIGS MIGHT FLY', logtxt))
|
||||
|
||||
def test02_count_true_args(self):
|
||||
self.assertEqual( count_true_args(), 0 )
|
||||
self.assertEqual( count_true_args(True), 1 )
|
||||
self.assertEqual( count_true_args(False), 0 )
|
||||
self.assertEqual( count_true_args(0,1,2,3), 3 )
|
||||
self.assertEqual(count_true_args(), 0)
|
||||
self.assertEqual(count_true_args(True), 1)
|
||||
self.assertEqual(count_true_args(False), 0)
|
||||
self.assertEqual(count_true_args(0, 1, 2, 3), 3)
|
||||
|
||||
def test03_parse_links(self):
|
||||
self.assertEqual( parse_links( [] ), [] )
|
||||
self.assertEqual( parse_links( ['u,h'] ), [{'href': 'h', 'rel': 'u'}] )
|
||||
self.assertEqual( parse_links( ['u,h','v,i'] ), [{'href': 'h', 'rel': 'u'},{'href': 'i', 'rel': 'v'}] )
|
||||
self.assertRaises( ClientFatalError, parse_links, 'xx' )
|
||||
self.assertRaises( ClientFatalError, parse_links, ['u'] )
|
||||
self.assertRaises( ClientFatalError, parse_links, ['u,h','u'] )
|
||||
self.assertEqual(parse_links([]), [])
|
||||
self.assertEqual(parse_links(['u,h']), [{'href': 'h', 'rel': 'u'}])
|
||||
self.assertEqual(parse_links(['u,h', 'v,i']), [
|
||||
{'href': 'h', 'rel': 'u'}, {'href': 'i', 'rel': 'v'}])
|
||||
self.assertRaises(ClientFatalError, parse_links, 'xx')
|
||||
self.assertRaises(ClientFatalError, parse_links, ['u'])
|
||||
self.assertRaises(ClientFatalError, parse_links, ['u,h', 'u'])
|
||||
|
||||
def test04_parse_link(self):
|
||||
# Input string of the form: rel,href,att1=val1,att2=val2
|
||||
self.assertEqual( parse_link('u,h'), {'href': 'h', 'rel': 'u'} )
|
||||
self.assertEqual( parse_link('u,h,a=b'), {'a': 'b', 'href': 'h', 'rel': 'u'} )
|
||||
self.assertEqual( parse_link('u,h,a=b,c=d'), {'a': 'b', 'c': 'd', 'href': 'h', 'rel': 'u'} )
|
||||
self.assertEqual( parse_link('u,h,a=b,a=d'), {'a': 'd', 'href': 'h', 'rel': 'u'} ) # desired??
|
||||
self.assertRaises( ClientFatalError, parse_link, '' )
|
||||
self.assertRaises( ClientFatalError, parse_link, 'u' )
|
||||
self.assertRaises( ClientFatalError, parse_link, 'u,' )
|
||||
self.assertRaises( ClientFatalError, parse_link, 'u,h,,' )
|
||||
self.assertRaises( ClientFatalError, parse_link, 'u,h,a' )
|
||||
self.assertRaises( ClientFatalError, parse_link, 'u,h,a=' )
|
||||
self.assertRaises( ClientFatalError, parse_link, 'u,h,a=b,=c' )
|
||||
self.assertEqual(parse_link('u,h'), {'href': 'h', 'rel': 'u'})
|
||||
self.assertEqual(parse_link('u,h,a=b'), {
|
||||
'a': 'b', 'href': 'h', 'rel': 'u'})
|
||||
self.assertEqual(parse_link('u,h,a=b,c=d'), {
|
||||
'a': 'b', 'c': 'd', 'href': 'h', 'rel': 'u'})
|
||||
self.assertEqual(parse_link('u,h,a=b,a=d'), {
|
||||
'a': 'd', 'href': 'h', 'rel': 'u'}) # desired??
|
||||
self.assertRaises(ClientFatalError, parse_link, '')
|
||||
self.assertRaises(ClientFatalError, parse_link, 'u')
|
||||
self.assertRaises(ClientFatalError, parse_link, 'u,')
|
||||
self.assertRaises(ClientFatalError, parse_link, 'u,h,,')
|
||||
self.assertRaises(ClientFatalError, parse_link, 'u,h,a')
|
||||
self.assertRaises(ClientFatalError, parse_link, 'u,h,a=')
|
||||
self.assertRaises(ClientFatalError, parse_link, 'u,h,a=b,=c')
|
||||
|
||||
def test05_parse_capabilities(self):
|
||||
# Input string of the form: cap_name=uri,cap_name=uri
|
||||
# Input string of the form: cap_name=uri,cap_name=uri
|
||||
# good
|
||||
c = parse_capabilities( 'a=' )
|
||||
self.assertEqual( len(c), 1 )
|
||||
self.assertEqual( c['a'], '' )
|
||||
c = parse_capabilities( 'a=b,c=' )
|
||||
self.assertEqual( len(c), 2 )
|
||||
self.assertEqual( c['a'], 'b' )
|
||||
c = parse_capabilities('a=')
|
||||
self.assertEqual(len(c), 1)
|
||||
self.assertEqual(c['a'], '')
|
||||
c = parse_capabilities('a=b,c=')
|
||||
self.assertEqual(len(c), 2)
|
||||
self.assertEqual(c['a'], 'b')
|
||||
# bad
|
||||
self.assertRaises( ClientFatalError, parse_capabilities, 'a' )
|
||||
self.assertRaises( ClientFatalError, parse_capabilities, 'a=b,' )
|
||||
self.assertRaises(ClientFatalError, parse_capabilities, 'a')
|
||||
self.assertRaises(ClientFatalError, parse_capabilities, 'a=b,')
|
||||
|
||||
def test06_parse_capability_lists(self):
|
||||
# Input string of the form: uri,uri
|
||||
self.assertEqual( parse_capability_lists('a,b'), ['a','b'] )
|
||||
self.assertEqual(parse_capability_lists('a,b'), ['a', 'b'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestClientUtils)
|
||||
|
||||
@ -12,151 +12,158 @@ from resync.resource import Resource
|
||||
from resync.resource_dump_manifest import ResourceDumpManifest
|
||||
from resync.change_dump_manifest import ChangeDumpManifest
|
||||
|
||||
|
||||
class TestDump(TestCase):
|
||||
|
||||
def test00_dump_zip_resource_list(self):
|
||||
rl=ResourceDumpManifest()
|
||||
rl.add( Resource('http://ex.org/a', length=7, path='tests/testdata/a') )
|
||||
rl.add( Resource('http://ex.org/b', length=21, path='tests/testdata/b') )
|
||||
d=Dump()
|
||||
zipf=os.path.join(self.tmpdir,"test00_dump.zip")
|
||||
d.write_zip(resources=rl,dumpfile=zipf) # named args
|
||||
self.assertTrue( os.path.exists(zipf) )
|
||||
self.assertTrue( zipfile.is_zipfile(zipf) )
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( len(zo.namelist()), 3 )
|
||||
rl = ResourceDumpManifest()
|
||||
rl.add(Resource('http://ex.org/a', length=7, path='tests/testdata/a'))
|
||||
rl.add(Resource('http://ex.org/b', length=21, path='tests/testdata/b'))
|
||||
d = Dump()
|
||||
zipf = os.path.join(self.tmpdir, "test00_dump.zip")
|
||||
d.write_zip(resources=rl, dumpfile=zipf) # named args
|
||||
self.assertTrue(os.path.exists(zipf))
|
||||
self.assertTrue(zipfile.is_zipfile(zipf))
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(len(zo.namelist()), 3)
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
|
||||
def test01_dump_zip_change_list(self):
|
||||
cl=ChangeDumpManifest()
|
||||
cl.add( Resource('http://ex.org/a', length=7, path='tests/testdata/a', change="updated") )
|
||||
cl.add( Resource('http://ex.org/b', length=21, path='tests/testdata/b', change="updated") )
|
||||
d=Dump()
|
||||
zipf=os.path.join(self.tmpdir,"test01_dump.zip")
|
||||
d.write_zip(cl,zipf) # positional args
|
||||
self.assertTrue( os.path.exists(zipf) )
|
||||
self.assertTrue( zipfile.is_zipfile(zipf) )
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( len(zo.namelist()), 3 )
|
||||
cl = ChangeDumpManifest()
|
||||
cl.add(Resource('http://ex.org/a', length=7,
|
||||
path='tests/testdata/a', change="updated"))
|
||||
cl.add(Resource('http://ex.org/b', length=21,
|
||||
path='tests/testdata/b', change="updated"))
|
||||
d = Dump()
|
||||
zipf = os.path.join(self.tmpdir, "test01_dump.zip")
|
||||
d.write_zip(cl, zipf) # positional args
|
||||
self.assertTrue(os.path.exists(zipf))
|
||||
self.assertTrue(zipfile.is_zipfile(zipf))
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(len(zo.namelist()), 3)
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
|
||||
def test02_dump_check_files(self):
|
||||
cl=ChangeList()
|
||||
cl.add( Resource('http://ex.org/a', length=7, path='tests/testdata/a', change="updated") )
|
||||
cl.add( Resource('http://ex.org/b', length=21, path='tests/testdata/b', change="updated") )
|
||||
d=Dump(resources=cl)
|
||||
cl = ChangeList()
|
||||
cl.add(Resource('http://ex.org/a', length=7,
|
||||
path='tests/testdata/a', change="updated"))
|
||||
cl.add(Resource('http://ex.org/b', length=21,
|
||||
path='tests/testdata/b', change="updated"))
|
||||
d = Dump(resources=cl)
|
||||
self.assertTrue(d.check_files())
|
||||
self.assertEqual(d.total_size, 28)
|
||||
|
||||
|
||||
def test03_dump_multi_file_max_size(self):
|
||||
rl=ResourceList()
|
||||
for letter in map(chr,range(ord('a'),ord('l')+1)):
|
||||
uri='http://ex.org/%s' % (letter)
|
||||
fname='tests/testdata/a_to_z/%s' % (letter)
|
||||
rl.add( Resource(uri, path=fname) )
|
||||
self.assertEqual( len(rl), 12 )
|
||||
#d=Dump(rl)
|
||||
#tmpdir=tempfile.mkdtemp()
|
||||
#tmpbase=os.path.join(tmpdir,'base')
|
||||
#d.max_size=2000 # start new zip after size exceeds 2000 bytes
|
||||
#n=d.write(tmpbase)
|
||||
#self.assertEqual( n, 2, 'expect to write 2 dump files' )
|
||||
#
|
||||
rl = ResourceList()
|
||||
for letter in map(chr, range(ord('a'), ord('l') + 1)):
|
||||
uri = 'http://ex.org/%s' % (letter)
|
||||
fname = 'tests/testdata/a_to_z/%s' % (letter)
|
||||
rl.add(Resource(uri, path=fname))
|
||||
self.assertEqual(len(rl), 12)
|
||||
# d=Dump(rl)
|
||||
# tmpdir=tempfile.mkdtemp()
|
||||
# tmpbase=os.path.join(tmpdir,'base')
|
||||
# d.max_size=2000 # start new zip after size exceeds 2000 bytes
|
||||
# n=d.write(tmpbase)
|
||||
# self.assertEqual( n, 2, 'expect to write 2 dump files' )
|
||||
#
|
||||
# Now repeat with large size limit but small number of files limit
|
||||
d2=Dump(rl)
|
||||
tmpbase=os.path.join(self.tmpdir,'test03_')
|
||||
d2.max_files=4
|
||||
n=d2.write(tmpbase)
|
||||
self.assertEqual( n, 3, 'expect to write 3 dump files' )
|
||||
self.assertTrue( os.path.isfile(tmpbase+'00000.zip') )
|
||||
self.assertTrue( os.path.isfile(tmpbase+'00001.zip') )
|
||||
self.assertTrue( os.path.isfile(tmpbase+'00002.zip') )
|
||||
d2 = Dump(rl)
|
||||
tmpbase = os.path.join(self.tmpdir, 'test03_')
|
||||
d2.max_files = 4
|
||||
n = d2.write(tmpbase)
|
||||
self.assertEqual(n, 3, 'expect to write 3 dump files')
|
||||
self.assertTrue(os.path.isfile(tmpbase + '00000.zip'))
|
||||
self.assertTrue(os.path.isfile(tmpbase + '00001.zip'))
|
||||
self.assertTrue(os.path.isfile(tmpbase + '00002.zip'))
|
||||
# Look at the first file in detail
|
||||
zipf=tmpbase+'00000.zip'
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( zo.namelist(), ['manifest.xml','a','b','c','d'] )
|
||||
#self.assertEqual( zo.getinfo('manifest.xml').file_size, 470 )
|
||||
self.assertEqual( zo.getinfo('a').file_size, 9 )
|
||||
self.assertEqual( zo.getinfo('b').file_size, 1116 )
|
||||
self.assertEqual( zo.getinfo('c').file_size, 32 )
|
||||
self.assertEqual( zo.getinfo('d').file_size, 13 )
|
||||
zipf = tmpbase + '00000.zip'
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(zo.namelist(), ['manifest.xml', 'a', 'b', 'c', 'd'])
|
||||
# self.assertEqual( zo.getinfo('manifest.xml').file_size, 470 )
|
||||
self.assertEqual(zo.getinfo('a').file_size, 9)
|
||||
self.assertEqual(zo.getinfo('b').file_size, 1116)
|
||||
self.assertEqual(zo.getinfo('c').file_size, 32)
|
||||
self.assertEqual(zo.getinfo('d').file_size, 13)
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
# Check second and third files have expected contents
|
||||
zipf=tmpbase+'00001.zip'
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( zo.namelist(), ['manifest.xml','e','f','g','h'] )
|
||||
zipf = tmpbase + '00001.zip'
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(zo.namelist(), ['manifest.xml', 'e', 'f', 'g', 'h'])
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
zipf=tmpbase+'00002.zip'
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( zo.namelist(), ['manifest.xml','i','j','k','l'] )
|
||||
zipf = tmpbase + '00002.zip'
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(zo.namelist(), ['manifest.xml', 'i', 'j', 'k', 'l'])
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
|
||||
def test04_dump_multi_file_max_size(self):
|
||||
rl=ResourceList()
|
||||
for letter in map(chr,range(ord('a'),ord('l')+1)):
|
||||
uri='http://ex.org/%s' % (letter)
|
||||
fname='tests/testdata/a_to_z/%s' % (letter)
|
||||
rl.add( Resource(uri, path=fname) )
|
||||
self.assertEqual( len(rl), 12 )
|
||||
d2=Dump(rl)
|
||||
tmpbase=os.path.join(self.tmpdir,'test0f_')
|
||||
d2.max_size=2000
|
||||
n=d2.write(tmpbase)
|
||||
self.assertEqual( n, 2, 'expect to write 2 dump files' )
|
||||
self.assertTrue( os.path.isfile(tmpbase+'00000.zip') )
|
||||
self.assertTrue( os.path.isfile(tmpbase+'00001.zip') )
|
||||
rl = ResourceList()
|
||||
for letter in map(chr, range(ord('a'), ord('l') + 1)):
|
||||
uri = 'http://ex.org/%s' % (letter)
|
||||
fname = 'tests/testdata/a_to_z/%s' % (letter)
|
||||
rl.add(Resource(uri, path=fname))
|
||||
self.assertEqual(len(rl), 12)
|
||||
d2 = Dump(rl)
|
||||
tmpbase = os.path.join(self.tmpdir, 'test0f_')
|
||||
d2.max_size = 2000
|
||||
n = d2.write(tmpbase)
|
||||
self.assertEqual(n, 2, 'expect to write 2 dump files')
|
||||
self.assertTrue(os.path.isfile(tmpbase + '00000.zip'))
|
||||
self.assertTrue(os.path.isfile(tmpbase + '00001.zip'))
|
||||
# Look at the first file in detail
|
||||
zipf=tmpbase+'00000.zip'
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( zo.namelist(), ['manifest.xml','a','b','c','d','e','f'] )
|
||||
#self.assertEqual( zo.getinfo('manifest.xml').file_size, 470 )
|
||||
self.assertEqual( zo.getinfo('a').file_size, 9 )
|
||||
self.assertEqual( zo.getinfo('b').file_size, 1116 )
|
||||
self.assertEqual( zo.getinfo('c').file_size, 32 )
|
||||
self.assertEqual( zo.getinfo('d').file_size, 13 )
|
||||
self.assertEqual( zo.getinfo('e').file_size, 20 )
|
||||
self.assertEqual( zo.getinfo('f').file_size, 1625 )
|
||||
zipf = tmpbase + '00000.zip'
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(
|
||||
zo.namelist(), ['manifest.xml', 'a', 'b', 'c', 'd', 'e', 'f'])
|
||||
# self.assertEqual( zo.getinfo('manifest.xml').file_size, 470 )
|
||||
self.assertEqual(zo.getinfo('a').file_size, 9)
|
||||
self.assertEqual(zo.getinfo('b').file_size, 1116)
|
||||
self.assertEqual(zo.getinfo('c').file_size, 32)
|
||||
self.assertEqual(zo.getinfo('d').file_size, 13)
|
||||
self.assertEqual(zo.getinfo('e').file_size, 20)
|
||||
self.assertEqual(zo.getinfo('f').file_size, 1625)
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
# Check second and third files have expected contents
|
||||
zipf=tmpbase+'00001.zip'
|
||||
zo=zipfile.ZipFile(zipf,'r')
|
||||
self.assertEqual( zo.namelist(), ['manifest.xml','g','h','i','j','k','l'] )
|
||||
zipf = tmpbase + '00001.zip'
|
||||
zo = zipfile.ZipFile(zipf, 'r')
|
||||
self.assertEqual(
|
||||
zo.namelist(), ['manifest.xml', 'g', 'h', 'i', 'j', 'k', 'l'])
|
||||
zo.close()
|
||||
os.unlink(zipf)
|
||||
|
||||
def test10_no_path(self):
|
||||
rl=ResourceList()
|
||||
rl.add( Resource('http://ex.org/a', length=7, path='tests/testdata/a') )
|
||||
rl.add( Resource('http://ex.org/b', length=21 ) )
|
||||
d=Dump(rl)
|
||||
self.assertRaises( DumpError, d.check_files )
|
||||
rl = ResourceList()
|
||||
rl.add(Resource('http://ex.org/a', length=7, path='tests/testdata/a'))
|
||||
rl.add(Resource('http://ex.org/b', length=21))
|
||||
d = Dump(rl)
|
||||
self.assertRaises(DumpError, d.check_files)
|
||||
|
||||
def test11_bad_size(self):
|
||||
rl=ResourceList()
|
||||
rl.add( Resource('http://ex.org/a', length=9999, path='tests/testdata/a') )
|
||||
d=Dump(rl)
|
||||
self.assertTrue( d.check_files(check_length=False) )
|
||||
self.assertRaises( DumpError, d.check_files )
|
||||
rl = ResourceList()
|
||||
rl.add(Resource('http://ex.org/a', length=9999, path='tests/testdata/a'))
|
||||
d = Dump(rl)
|
||||
self.assertTrue(d.check_files(check_length=False))
|
||||
self.assertRaises(DumpError, d.check_files)
|
||||
|
||||
def test10_no_path(self):
|
||||
rl=ResourceList()
|
||||
rl.add( Resource('http://ex.org/a', length=7, path='tests/testdata/a') )
|
||||
rl.add( Resource('http://ex.org/b', length=21 ) )
|
||||
d=Dump(rl)
|
||||
self.assertRaises( DumpError, d.check_files )
|
||||
rl = ResourceList()
|
||||
rl.add(Resource('http://ex.org/a', length=7, path='tests/testdata/a'))
|
||||
rl.add(Resource('http://ex.org/b', length=21))
|
||||
d = Dump(rl)
|
||||
self.assertRaises(DumpError, d.check_files)
|
||||
|
||||
def test11_bad_size(self):
|
||||
rl=ResourceList()
|
||||
rl.add( Resource('http://ex.org/a', length=9999, path='tests/testdata/a') )
|
||||
d=Dump(rl)
|
||||
self.assertTrue( d.check_files(check_length=False) )
|
||||
self.assertRaises( DumpError, d.check_files )
|
||||
rl = ResourceList()
|
||||
rl.add(Resource('http://ex.org/a', length=9999, path='tests/testdata/a'))
|
||||
d = Dump(rl)
|
||||
self.assertTrue(d.check_files(check_length=False))
|
||||
self.assertRaises(DumpError, d.check_files)
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDump)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -21,23 +21,23 @@ class TestExplorer(unittest.TestCase):
|
||||
def test01_create(self):
|
||||
# dumb test that we can create Exporer object
|
||||
e = Explorer()
|
||||
self.assertTrue( e )
|
||||
self.assertTrue(e)
|
||||
|
||||
def test01_xresource(self):
|
||||
x = XResource( uri='abc', acceptable_capabilities=[1,2],
|
||||
checks=[3,4], context='http://example.org/def' )
|
||||
self.assertEqual( x.uri, 'http://example.org/abc' )
|
||||
self.assertEqual( x.acceptable_capabilities, [1,2] )
|
||||
self.assertEqual( x.checks, [3,4] )
|
||||
x = XResource(uri='abc', acceptable_capabilities=[1, 2],
|
||||
checks=[3, 4], context='http://example.org/def')
|
||||
self.assertEqual(x.uri, 'http://example.org/abc')
|
||||
self.assertEqual(x.acceptable_capabilities, [1, 2])
|
||||
self.assertEqual(x.checks, [3, 4])
|
||||
|
||||
def test02_head_response(self):
|
||||
hr = HeadResponse()
|
||||
self.assertEqual( hr.status_code, None )
|
||||
self.assertEqual( len(hr.headers), 0)
|
||||
self.assertEqual(hr.status_code, None)
|
||||
self.assertEqual(len(hr.headers), 0)
|
||||
|
||||
def test03_explorer_quit(self):
|
||||
eq = ExplorerQuit()
|
||||
self.assertTrue( isinstance(eq, Exception) )
|
||||
self.assertTrue(isinstance(eq, Exception))
|
||||
|
||||
def test04_explore(self):
|
||||
e = Explorer()
|
||||
@ -46,81 +46,90 @@ class TestExplorer(unittest.TestCase):
|
||||
# IOError generated from attempt to read stdin
|
||||
e.fake_input = 'q'
|
||||
e.explore()
|
||||
self.assertTrue( re.search(r'resync-explorer done', capturer.result) )
|
||||
self.assertTrue(re.search(r'resync-explorer done', capturer.result))
|
||||
|
||||
def test05_explore_uri(self):
|
||||
e = Explorer()
|
||||
with capture_stdout() as capturer:
|
||||
e.fake_input = 'q'
|
||||
self.assertRaises( ExplorerQuit, e.explore_uri,
|
||||
XResource('tests/testdata/explore1/caps1.xml') )
|
||||
self.assertTrue( re.search(r'Reading tests/testdata/explore1/caps1.xml',
|
||||
capturer.result) )
|
||||
self.assertTrue( re.search(r'Parsed capabilitylist document with 4 entries:',
|
||||
capturer.result) )
|
||||
self.assertRaises(ExplorerQuit, e.explore_uri,
|
||||
XResource('tests/testdata/explore1/caps1.xml'))
|
||||
self.assertTrue(re.search(r'Reading tests/testdata/explore1/caps1.xml',
|
||||
capturer.result))
|
||||
self.assertTrue(re.search(r'Parsed capabilitylist document with 4 entries:',
|
||||
capturer.result))
|
||||
|
||||
def test06_explore_show_summary(self):
|
||||
e = Explorer()
|
||||
# file that exists with matching
|
||||
with capture_stdout() as capturer:
|
||||
e.explore_show_summary( list=CapabilityList() )
|
||||
self.assertTrue( re.search(r'Parsed \(unknown capability\) document with 0 entries:',
|
||||
capturer.result) )
|
||||
e.explore_show_summary(list=CapabilityList())
|
||||
self.assertTrue(re.search(r'Parsed \(unknown capability\) document with 0 entries:',
|
||||
capturer.result))
|
||||
# dummy capabilities object and display
|
||||
cl = CapabilityList()
|
||||
cl.add( Resource('uri:resourcelist') )
|
||||
cl.add( Resource('uri:changelist') )
|
||||
cl.add(Resource('uri:resourcelist'))
|
||||
cl.add(Resource('uri:changelist'))
|
||||
with capture_stdout() as capturer:
|
||||
e.explore_show_summary(cl,False,[])
|
||||
self.assertTrue( re.search(r'Parsed \(unknown capability\) document with 2 entries:',capturer.result) )
|
||||
self.assertTrue( re.search(r'\[1\] uri:changelist',capturer.result) )
|
||||
self.assertTrue( re.search(r'\[2\] uri:resourcelist',capturer.result) )
|
||||
|
||||
e.explore_show_summary(cl, False, [])
|
||||
self.assertTrue(re.search(
|
||||
r'Parsed \(unknown capability\) document with 2 entries:', capturer.result))
|
||||
self.assertTrue(re.search(r'\[1\] uri:changelist', capturer.result))
|
||||
self.assertTrue(re.search(r'\[2\] uri:resourcelist', capturer.result))
|
||||
|
||||
def test07_explore_show_head(self):
|
||||
e = Explorer()
|
||||
# file that exists with matching
|
||||
with capture_stdout() as capturer:
|
||||
e.explore_show_head( uri='tests/testdata/dir1/file_a',
|
||||
check_headers={ 'content-length': 20,
|
||||
'unknown': 'abc' } )
|
||||
self.assertTrue( re.search(r'HEAD tests/testdata/dir1/file_a', capturer.result) )
|
||||
self.assertTrue( re.search(r'content-length: 20 MATCHES EXPECTED VALUE', capturer.result) )
|
||||
e.explore_show_head(uri='tests/testdata/dir1/file_a',
|
||||
check_headers={'content-length': 20,
|
||||
'unknown': 'abc'})
|
||||
self.assertTrue(
|
||||
re.search(r'HEAD tests/testdata/dir1/file_a', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'content-length: 20 MATCHES EXPECTED VALUE', capturer.result))
|
||||
# same file, bad header check
|
||||
with capture_stdout() as capturer:
|
||||
e.explore_show_head( 'tests/testdata/dir1/file_a',
|
||||
check_headers={ 'content-length': 99 } )
|
||||
self.assertTrue( re.search(r'HEAD tests/testdata/dir1/file_a', capturer.result) )
|
||||
self.assertTrue( re.search(r'content-length: 20 EXPECTED 99', capturer.result) )
|
||||
e.explore_show_head('tests/testdata/dir1/file_a',
|
||||
check_headers={'content-length': 99})
|
||||
self.assertTrue(
|
||||
re.search(r'HEAD tests/testdata/dir1/file_a', capturer.result))
|
||||
self.assertTrue(
|
||||
re.search(r'content-length: 20 EXPECTED 99', capturer.result))
|
||||
# file that does not exist
|
||||
with capture_stdout() as capturer:
|
||||
e.explore_show_head( 'tests/testdata/does_not_exist' )
|
||||
self.assertTrue( re.search(r'HEAD tests/testdata/does_not_exist', capturer.result) )
|
||||
self.assertTrue( re.search(r'status: 404', capturer.result) )
|
||||
e.explore_show_head('tests/testdata/does_not_exist')
|
||||
self.assertTrue(
|
||||
re.search(r'HEAD tests/testdata/does_not_exist', capturer.result))
|
||||
self.assertTrue(re.search(r'status: 404', capturer.result))
|
||||
|
||||
def test08_head_on_file(self):
|
||||
e = Explorer()
|
||||
r1 = e.head_on_file('tests/testdata/does_not_exist')
|
||||
self.assertEqual( r1.status_code, '404' )
|
||||
self.assertEqual(r1.status_code, '404')
|
||||
r2 = e.head_on_file('tests/testdata/dir1/file_a')
|
||||
self.assertEqual( r2.status_code, '200' )
|
||||
self.assertTrue( re.match(r'^\d\d\d\d\-\d\d\-\d\d', r2.headers['last-modified']) )
|
||||
self.assertEqual( r2.headers['content-length'], 20 )
|
||||
self.assertEqual(r2.status_code, '200')
|
||||
self.assertTrue(re.match(r'^\d\d\d\d\-\d\d\-\d\d',
|
||||
r2.headers['last-modified']))
|
||||
self.assertEqual(r2.headers['content-length'], 20)
|
||||
|
||||
def test09_allowed_entries(self):
|
||||
e = Explorer()
|
||||
self.assertEqual( e.allowed_entries('capabilitylistindex'), [] )
|
||||
self.assertEqual( e.allowed_entries('resourcelistindex'), ['resourcelist'] )
|
||||
self.assertEqual( e.allowed_entries('changelist-archive'), ['changelist'] )
|
||||
self.assertEqual( e.allowed_entries('description'),['capabilitylist'] )
|
||||
self.assertEqual( len(e.allowed_entries('capabilitylist')), 8 )
|
||||
self.assertEqual( e.allowed_entries('unknown'),[] )
|
||||
self.assertEqual(e.allowed_entries('capabilitylistindex'), [])
|
||||
self.assertEqual(e.allowed_entries(
|
||||
'resourcelistindex'), ['resourcelist'])
|
||||
self.assertEqual(e.allowed_entries(
|
||||
'changelist-archive'), ['changelist'])
|
||||
self.assertEqual(e.allowed_entries('description'), ['capabilitylist'])
|
||||
self.assertEqual(len(e.allowed_entries('capabilitylist')), 8)
|
||||
self.assertEqual(e.allowed_entries('unknown'), [])
|
||||
|
||||
def test10_expand_relative_uri(self):
|
||||
e = Explorer()
|
||||
with capture_stdout() as capturer:
|
||||
uri = e.expand_relative_uri('https://example.org/ctx','abc')
|
||||
self.assertEqual( uri, 'https://example.org/abc' )
|
||||
self.assertTrue( re.search(r'expanded relative URI', capturer.result) )
|
||||
uri = e.expand_relative_uri('https://example.org/ctx', 'abc')
|
||||
self.assertEqual(uri, 'https://example.org/abc')
|
||||
self.assertTrue(re.search(r'expanded relative URI', capturer.result))
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestExplorer)
|
||||
|
||||
@ -3,11 +3,11 @@ from tests.testcase_with_tmpdir import TestCase
|
||||
import sys
|
||||
import os.path
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
|
||||
from resync.resource import Resource
|
||||
@ -16,38 +16,39 @@ from resync.sitemap import Sitemap, SitemapIndexError
|
||||
|
||||
# etree gives ParseError in 2.7, ExpatError in 2.6
|
||||
etree_error_class = None
|
||||
if (sys.version_info < (2,7)):
|
||||
if (sys.version_info < (2, 7)):
|
||||
from xml.parsers.expat import ExpatError
|
||||
etree_error_class = ExpatError
|
||||
else:
|
||||
from xml.etree.ElementTree import ParseError
|
||||
etree_error_class = ParseError
|
||||
|
||||
|
||||
class TestListBase(TestCase):
|
||||
|
||||
def test01_len_count(self):
|
||||
# count sets explicit number of resources, len(resources) not used
|
||||
lb = ListBase()
|
||||
self.assertEqual( len(lb), 0 )
|
||||
self.assertEqual(len(lb), 0)
|
||||
lb.add(Resource('a'))
|
||||
self.assertEqual( len(lb), 1 )
|
||||
lb = ListBase( count=100 )
|
||||
self.assertEqual( len(lb), 100 )
|
||||
self.assertEqual(len(lb), 1)
|
||||
lb = ListBase(count=100)
|
||||
self.assertEqual(len(lb), 100)
|
||||
|
||||
def test02_print(self):
|
||||
lb = ListBase()
|
||||
lb.add( Resource(uri='a',lastmod='2001-01-01',length=1234) )
|
||||
lb.add( Resource(uri='b',lastmod='2002-02-02',length=56789) )
|
||||
lb.add( Resource(uri='c',lastmod='2003-03-03',length=0) )
|
||||
lb.md['from']=None #avoid now being added
|
||||
lb.add(Resource(uri='a', lastmod='2001-01-01', length=1234))
|
||||
lb.add(Resource(uri='b', lastmod='2002-02-02', length=56789))
|
||||
lb.add(Resource(uri='c', lastmod='2003-03-03', length=0))
|
||||
lb.md['from'] = None # avoid now being added
|
||||
x = lb.as_xml()
|
||||
self.assertEqual( x, '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length="1234" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length="56789" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length="0" /></url></urlset>' )
|
||||
self.assertEqual(x, '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length="1234" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length="56789" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length="0" /></url></urlset>')
|
||||
|
||||
def test03_read(self):
|
||||
# just does parse
|
||||
lb=ListBase()
|
||||
lb = ListBase()
|
||||
lb.read(uri='tests/testdata/unknown/unknown1.xml')
|
||||
self.assertEqual( len(lb.resources), 2 )
|
||||
self.assertEqual(len(lb.resources), 2)
|
||||
|
||||
def test04_parse(self):
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
@ -57,38 +58,38 @@ class TestListBase(TestCase):
|
||||
<url><loc>/tmp/rs_test/src/file_b</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="32" /></url>\
|
||||
</urlset>'
|
||||
# parse from string
|
||||
lb1=ListBase()
|
||||
lb1 = ListBase()
|
||||
lb1.parse(str_data=xml)
|
||||
self.assertEqual( len(lb1.resources), 2, 'got 2 resources')
|
||||
self.assertEqual(len(lb1.resources), 2, 'got 2 resources')
|
||||
# parse from fh
|
||||
lb2=ListBase()
|
||||
lb2 = ListBase()
|
||||
lb2.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(lb2.resources), 2, 'got 2 resources')
|
||||
self.assertEqual(len(lb2.resources), 2, 'got 2 resources')
|
||||
# parse from string (LEGACY parameter name, to be removed)
|
||||
lb3=ListBase()
|
||||
lb3 = ListBase()
|
||||
lb3.parse(str=xml)
|
||||
self.assertEqual( len(lb3.resources), 2, 'got 2 resources')
|
||||
self.assertEqual(len(lb3.resources), 2, 'got 2 resources')
|
||||
# file that doesn't exits
|
||||
lb4 = ListBase()
|
||||
self.assertRaises( Exception, lb4.parse,
|
||||
uri='tests/testdata/does_not_exist' )
|
||||
self.assertRaises(Exception, lb4.parse,
|
||||
uri='tests/testdata/does_not_exist')
|
||||
# nothing
|
||||
self.assertRaises( Exception, lb4.parse )
|
||||
self.assertRaises(Exception, lb4.parse)
|
||||
|
||||
def test04_write(self):
|
||||
lb = ListBase(capability_name = 'special')
|
||||
lb.add( Resource(uri='http://example.org/lemon') )
|
||||
lb.add( Resource(uri='http://example.org/orange') )
|
||||
basename = os.path.join(self.tmpdir,'lb.xml')
|
||||
lb = ListBase(capability_name='special')
|
||||
lb.add(Resource(uri='http://example.org/lemon'))
|
||||
lb.add(Resource(uri='http://example.org/orange'))
|
||||
basename = os.path.join(self.tmpdir, 'lb.xml')
|
||||
lb.write(basename=basename)
|
||||
self.assertTrue( os.path.exists(basename) )
|
||||
self.assertTrue(os.path.exists(basename))
|
||||
# and now parse back
|
||||
fh = open(basename,'r')
|
||||
lb2 = ListBase(capability_name = 'special')
|
||||
fh = open(basename, 'r')
|
||||
lb2 = ListBase(capability_name='special')
|
||||
lb2.parse(fh=fh)
|
||||
self.assertEqual( lb2.capability, 'special' )
|
||||
self.assertEqual( len(lb2), 2 )
|
||||
self.assertEqual(lb2.capability, 'special')
|
||||
self.assertEqual(len(lb2), 2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestListBase)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
|
||||
from resync.resource import Resource
|
||||
@ -14,118 +14,122 @@ from resync.sitemap import Sitemap, SitemapIndexError
|
||||
|
||||
# etree gives ParseError in 2.7, ExpatError in 2.6
|
||||
etree_error_class = None
|
||||
if (sys.version_info < (2,7)):
|
||||
if (sys.version_info < (2, 7)):
|
||||
from xml.parsers.expat import ExpatError
|
||||
etree_error_class = ExpatError
|
||||
else:
|
||||
from xml.etree.ElementTree import ParseError
|
||||
etree_error_class = ParseError
|
||||
|
||||
|
||||
class TestListBaseWithIndex(unittest.TestCase):
|
||||
|
||||
def test01_print(self):
|
||||
lb = ListBaseWithIndex()
|
||||
lb.add( Resource(uri='a',lastmod='2001-01-01',length=1234) )
|
||||
lb.add( Resource(uri='b',lastmod='2002-02-02',length=56789) )
|
||||
lb.add( Resource(uri='c',lastmod='2003-03-03',length=0) )
|
||||
lb.md['from']=None #avoid now being added
|
||||
self.assertEqual( lb.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length="1234" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length="56789" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length="0" /></url></urlset>' )
|
||||
lb.add(Resource(uri='a', lastmod='2001-01-01', length=1234))
|
||||
lb.add(Resource(uri='b', lastmod='2002-02-02', length=56789))
|
||||
lb.add(Resource(uri='c', lastmod='2003-03-03', length=0))
|
||||
lb.md['from'] = None # avoid now being added
|
||||
self.assertEqual(lb.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length="1234" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length="56789" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length="0" /></url></urlset>')
|
||||
|
||||
def test02_print_iter(self):
|
||||
r = [ Resource(uri='a',lastmod='2001-01-01',length=1234),
|
||||
Resource(uri='b',lastmod='2002-02-02',length=56789),
|
||||
Resource(uri='c',lastmod='2003-03-03',length=0) ]
|
||||
r = [Resource(uri='a', lastmod='2001-01-01', length=1234),
|
||||
Resource(uri='b', lastmod='2002-02-02', length=56789),
|
||||
Resource(uri='c', lastmod='2003-03-03', length=0)]
|
||||
# without setting count will barf on len() attempt
|
||||
lb = ListBaseWithIndex( resources=iter(r) )
|
||||
self.assertRaises( TypeError, lb.as_xml )
|
||||
# set explicit count larger than max_sitemap_entiries and as_xml will throw exception
|
||||
lb = ListBaseWithIndex( resources=iter(r), count=3 )
|
||||
lb = ListBaseWithIndex(resources=iter(r))
|
||||
self.assertRaises(TypeError, lb.as_xml)
|
||||
# set explicit count larger than max_sitemap_entiries and as_xml will
|
||||
# throw exception
|
||||
lb = ListBaseWithIndex(resources=iter(r), count=3)
|
||||
lb.max_sitemap_entries = 2
|
||||
self.assertRaises( ListBaseIndexError, lb.as_xml )
|
||||
self.assertRaises(ListBaseIndexError, lb.as_xml)
|
||||
# set explicit count and all will be OK
|
||||
lb = ListBaseWithIndex( resources=iter(r), count=3 )
|
||||
lb.md['from']=None #avoid now being added
|
||||
self.assertEqual( lb.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length="1234" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length="56789" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length="0" /></url></urlset>' )
|
||||
lb = ListBaseWithIndex(resources=iter(r), count=3)
|
||||
lb.md['from'] = None # avoid now being added
|
||||
self.assertEqual(lb.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length="1234" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length="56789" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length="0" /></url></urlset>')
|
||||
|
||||
def test03_parse_2(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="unknown" from="2013-02-12T14:09:00Z" />\
|
||||
<url><loc>/tmp/rs_test/src/file_a</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="12" /></url>\
|
||||
<url><loc>/tmp/rs_test/src/file_b</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="32" /></url>\
|
||||
</urlset>'
|
||||
lb=ListBaseWithIndex()
|
||||
lb = ListBaseWithIndex()
|
||||
lb.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(lb.resources), 2, 'got 2 resources')
|
||||
|
||||
self.assertEqual(len(lb.resources), 2, 'got 2 resources')
|
||||
|
||||
def test16_as_xml(self):
|
||||
r = [ Resource(uri='a',lastmod='2014-04-14',length=14),
|
||||
Resource(uri='b',lastmod='2015-05-15',length=15),
|
||||
Resource(uri='c',lastmod='2016-06-16',length=16) ]
|
||||
lb=ListBaseWithIndex( resources=r )
|
||||
r = [Resource(uri='a', lastmod='2014-04-14', length=14),
|
||||
Resource(uri='b', lastmod='2015-05-15', length=15),
|
||||
Resource(uri='c', lastmod='2016-06-16', length=16)]
|
||||
lb = ListBaseWithIndex(resources=r)
|
||||
lb.max_sitemap_entries = 3
|
||||
# One file
|
||||
xml = lb.as_xml()
|
||||
self.assertTrue( re.search(r'<urlset ',xml) )
|
||||
self.assertTrue( re.search(r'<loc>a</loc>',xml) )
|
||||
self.assertTrue( re.search(r'<loc>b</loc>',xml) )
|
||||
self.assertTrue( re.search(r'<loc>c</loc>',xml) )
|
||||
self.assertTrue(re.search(r'<urlset ', xml))
|
||||
self.assertTrue(re.search(r'<loc>a</loc>', xml))
|
||||
self.assertTrue(re.search(r'<loc>b</loc>', xml))
|
||||
self.assertTrue(re.search(r'<loc>c</loc>', xml))
|
||||
# Needs multifile bit not allowed
|
||||
lb.max_sitemap_entries = 1
|
||||
self.assertRaises( ListBaseIndexError, lb.as_xml )
|
||||
self.assertRaises(ListBaseIndexError, lb.as_xml)
|
||||
# Allow multifile...
|
||||
xml = lb.as_xml( allow_multifile=True )
|
||||
self.assertTrue( re.search(r'<sitemapindex', xml) )
|
||||
self.assertTrue( re.search(r'<loc>/tmp/sitemap00001.xml</loc>', xml) )
|
||||
self.assertTrue( re.search(r'<loc>/tmp/sitemap00002.xml</loc>', xml) )
|
||||
self.assertFalse( re.search(r'<loc>/tmp/sitemap00003.xml</loc>', xml) )
|
||||
xml = lb.as_xml(allow_multifile=True)
|
||||
self.assertTrue(re.search(r'<sitemapindex', xml))
|
||||
self.assertTrue(re.search(r'<loc>/tmp/sitemap00001.xml</loc>', xml))
|
||||
self.assertTrue(re.search(r'<loc>/tmp/sitemap00002.xml</loc>', xml))
|
||||
self.assertFalse(re.search(r'<loc>/tmp/sitemap00003.xml</loc>', xml))
|
||||
|
||||
def test17_as_xml_index(self):
|
||||
r = [ Resource(uri='a',lastmod='2006-01-01',length=12),
|
||||
Resource(uri='b',lastmod='2007-02-02',length=34),
|
||||
Resource(uri='c',lastmod='2008-03-03',length=56) ]
|
||||
lb=ListBaseWithIndex( resources=r )
|
||||
r = [Resource(uri='a', lastmod='2006-01-01', length=12),
|
||||
Resource(uri='b', lastmod='2007-02-02', length=34),
|
||||
Resource(uri='c', lastmod='2008-03-03', length=56)]
|
||||
lb = ListBaseWithIndex(resources=r)
|
||||
lb.max_sitemap_entries = 2
|
||||
xml = lb.as_xml_index()
|
||||
self.assertTrue( re.search(r'<loc>/tmp/sitemap00001.xml</loc>', xml) )
|
||||
self.assertFalse( re.search(r'<loc>/tmp/sitemap00002.xml</loc>', xml) )
|
||||
self.assertTrue(re.search(r'<loc>/tmp/sitemap00001.xml</loc>', xml))
|
||||
self.assertFalse(re.search(r'<loc>/tmp/sitemap00002.xml</loc>', xml))
|
||||
# Index not required
|
||||
lb.max_sitemap_entries = 3
|
||||
self.assertRaises( ListBaseIndexError, lb.as_xml_index )
|
||||
self.assertRaises(ListBaseIndexError, lb.as_xml_index)
|
||||
|
||||
def test18_as_xml_part(self):
|
||||
r = [ Resource(uri='a',lastmod='2006-01-01',length=12),
|
||||
Resource(uri='b',lastmod='2007-02-02',length=34),
|
||||
Resource(uri='c',lastmod='2008-03-03',length=56) ]
|
||||
lb=ListBaseWithIndex( resources=r )
|
||||
r = [Resource(uri='a', lastmod='2006-01-01', length=12),
|
||||
Resource(uri='b', lastmod='2007-02-02', length=34),
|
||||
Resource(uri='c', lastmod='2008-03-03', length=56)]
|
||||
lb = ListBaseWithIndex(resources=r)
|
||||
# Allow unlimited entries, part makes no sense
|
||||
lb.max_sitemap_entries = None
|
||||
self.assertRaises( ListBaseIndexError, lb.as_xml_part )
|
||||
self.assertRaises(ListBaseIndexError, lb.as_xml_part)
|
||||
# Request after end
|
||||
lb.max_sitemap_entries = 1
|
||||
self.assertRaises( ListBaseIndexError, lb.as_xml_part, part_number=9 )
|
||||
self.assertRaises(ListBaseIndexError, lb.as_xml_part, part_number=9)
|
||||
# Allow only 1 entry
|
||||
lb.max_sitemap_entries = 1
|
||||
xml = lb.as_xml_part( part_number=1 )
|
||||
self.assertFalse( re.search(r'<loc>a</loc>',xml) )
|
||||
self.assertTrue( re.search(r'<loc>b</loc>',xml) )
|
||||
self.assertFalse( re.search(r'<loc>c</loc>',xml) )
|
||||
xml = lb.as_xml_part(part_number=1)
|
||||
self.assertFalse(re.search(r'<loc>a</loc>', xml))
|
||||
self.assertTrue(re.search(r'<loc>b</loc>', xml))
|
||||
self.assertFalse(re.search(r'<loc>c</loc>', xml))
|
||||
# Request truncated
|
||||
lb.max_sitemap_entries = 2
|
||||
xml = lb.as_xml_part( part_number=1 )
|
||||
self.assertFalse( re.search(r'<loc>a</loc>',xml) )
|
||||
self.assertFalse( re.search(r'<loc>b</loc>',xml) )
|
||||
self.assertTrue( re.search(r'<loc>c</loc>',xml) )
|
||||
xml = lb.as_xml_part(part_number=1)
|
||||
self.assertFalse(re.search(r'<loc>a</loc>', xml))
|
||||
self.assertFalse(re.search(r'<loc>b</loc>', xml))
|
||||
self.assertTrue(re.search(r'<loc>c</loc>', xml))
|
||||
|
||||
def test20_index_as_xml(self):
|
||||
# Check XML for empty case
|
||||
lb=ListBaseWithIndex()
|
||||
self.assertEqual( lb.index_as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /></sitemapindex>' )
|
||||
lb = ListBaseWithIndex()
|
||||
self.assertEqual(lb.index_as_xml(
|
||||
), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:md capability="unknown" /></sitemapindex>')
|
||||
# Add a resource and make sure we find that
|
||||
lb.add( Resource(uri='a',lastmod='2001-01-01',length=1234) )
|
||||
lb.add(Resource(uri='a', lastmod='2001-01-01', length=1234))
|
||||
xml = lb.index_as_xml()
|
||||
self.assertTrue( re.search(r'<loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod>',xml) )
|
||||
self.assertTrue(
|
||||
re.search(r'<loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod>', xml))
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestListBaseWithIndex)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -25,8 +25,8 @@ class TestMapper(unittest.TestCase):
|
||||
self.assertRaises(MapperError, m6.parse, ['a=b=c'])
|
||||
self.assertRaises(MapperError, m6.parse, ['a=b=c=d'])
|
||||
# dupes
|
||||
self.assertRaises(MapperError, m6.parse, ['a=b','a=c'])
|
||||
self.assertRaises(MapperError, m6.parse, ['x=z','y=z'])
|
||||
self.assertRaises(MapperError, m6.parse, ['a=b', 'a=c'])
|
||||
self.assertRaises(MapperError, m6.parse, ['x=z', 'y=z'])
|
||||
|
||||
def test01_mapper_src_to_dst(self):
|
||||
m = Mapper(['http://e.org/p/', '/tmp/q/'])
|
||||
|
||||
@ -2,19 +2,20 @@ import unittest
|
||||
import re
|
||||
from resync.resource import Resource, ChangeTypeError
|
||||
|
||||
|
||||
class TestResource(unittest.TestCase):
|
||||
|
||||
def test01a_same(self):
|
||||
r1 = Resource('a')
|
||||
r2 = Resource('a')
|
||||
self.assertEqual( r1, r1 )
|
||||
self.assertEqual( r1, r2 )
|
||||
self.assertEqual(r1, r1)
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test01b_same(self):
|
||||
r1 = Resource(uri='a',timestamp=1234.0)
|
||||
r2 = Resource(uri='a',timestamp=1234.0)
|
||||
self.assertEqual( r1, r1 )
|
||||
self.assertEqual( r1, r2 )
|
||||
r1 = Resource(uri='a', timestamp=1234.0)
|
||||
r2 = Resource(uri='a', timestamp=1234.0)
|
||||
self.assertEqual(r1, r1)
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test01c_same(self):
|
||||
"""Same with lastmod instead of direct timestamp"""
|
||||
@ -29,128 +30,132 @@ class TestResource(unittest.TestCase):
|
||||
'2012-01-01T00:00:00Z',
|
||||
'2012-01-01T00:00:00.000000Z',
|
||||
'2012-01-01T00:00:00.000000000000Z',
|
||||
'2012-01-01T00:00:00.000000000001Z', #below resolution
|
||||
'2012-01-01T00:00:00.000000000001Z', # below resolution
|
||||
'2012-01-01T00:00:00.00+00:00',
|
||||
'2012-01-01T00:00:00.00-00:00',
|
||||
'2012-01-01T02:00:00.00-02:00',
|
||||
'2011-12-31T23:00:00.00+01:00'
|
||||
):
|
||||
r2.lastmod = r2lm
|
||||
self.assertEqual( r1.timestamp, r2.timestamp, ('%s (%f) == %s (%f)' % (r1lm,r1.timestamp,r2lm,r2.timestamp)) )
|
||||
self.assertEqual( r1, r2 )
|
||||
self.assertEqual(r1.timestamp, r2.timestamp, ('%s (%f) == %s (%f)' % (
|
||||
r1lm, r1.timestamp, r2lm, r2.timestamp)))
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test01d_same(self):
|
||||
"""Same with slight timestamp diff"""
|
||||
r1 = Resource('a')
|
||||
r1.lastmod='2012-01-02T01:02:03Z'
|
||||
r1.lastmod = '2012-01-02T01:02:03Z'
|
||||
r2 = Resource('a')
|
||||
r2.lastmod='2012-01-02T01:02:03.99Z'
|
||||
self.assertNotEqual( r1.timestamp, r2.timestamp )
|
||||
self.assertEqual( r1, r2 )
|
||||
r2.lastmod = '2012-01-02T01:02:03.99Z'
|
||||
self.assertNotEqual(r1.timestamp, r2.timestamp)
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test02a_diff(self):
|
||||
r1 = Resource('a')
|
||||
r2 = Resource('b')
|
||||
self.assertNotEqual(r1,r2)
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test02b_diff(self):
|
||||
r1 = Resource('a',lastmod='2012-01-11')
|
||||
r2 = Resource('a',lastmod='2012-01-22')
|
||||
#print 'r1 == r2 : '+str(r1==r2)
|
||||
self.assertNotEqual( r1, r2 )
|
||||
r1 = Resource('a', lastmod='2012-01-11')
|
||||
r2 = Resource('a', lastmod='2012-01-22')
|
||||
# print 'r1 == r2 : '+str(r1==r2)
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test04_bad_lastmod(self):
|
||||
def setlastmod(r,v):
|
||||
r.lastmod=v
|
||||
def setlastmod(r, v):
|
||||
r.lastmod = v
|
||||
r = Resource('4')
|
||||
# Bad formats
|
||||
self.assertRaises( ValueError, setlastmod, r, "bad_lastmod" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-13-01" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-12-32" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-11-01T10:10:60" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-11-01T10:10:59.9x" )
|
||||
self.assertRaises(ValueError, setlastmod, r, "bad_lastmod")
|
||||
self.assertRaises(ValueError, setlastmod, r, "")
|
||||
self.assertRaises(ValueError, setlastmod, r, "2012-13-01")
|
||||
self.assertRaises(ValueError, setlastmod, r, "2012-12-32")
|
||||
self.assertRaises(ValueError, setlastmod, r, "2012-11-01T10:10:60")
|
||||
self.assertRaises(ValueError, setlastmod, r, "2012-11-01T10:10:59.9x")
|
||||
# Valid ISO8601 but not alloed in W3C Datetime
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-11-01T01:01:01" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-11-01 01:01:01Z" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-11-01T01:01:01+0000" )
|
||||
self.assertRaises( ValueError, setlastmod, r, "2012-11-01T01:01:01-1000" )
|
||||
|
||||
self.assertRaises(ValueError, setlastmod, r, "2012-11-01T01:01:01")
|
||||
self.assertRaises(ValueError, setlastmod, r, "2012-11-01 01:01:01Z")
|
||||
self.assertRaises(ValueError, setlastmod, r,
|
||||
"2012-11-01T01:01:01+0000")
|
||||
self.assertRaises(ValueError, setlastmod, r,
|
||||
"2012-11-01T01:01:01-1000")
|
||||
|
||||
def test05_lastmod_roundtrips(self):
|
||||
r = Resource('a')
|
||||
r.lastmod='2012-03-14'
|
||||
self.assertEqual( r.lastmod, '2012-03-14T00:00:00Z' )
|
||||
r.lastmod='2012-03-14T00:00:00+00:00'
|
||||
#print r.timestamp
|
||||
self.assertEqual( r.lastmod, '2012-03-14T00:00:00Z' )
|
||||
r.lastmod='2012-03-14T00:00:00-00:00'
|
||||
#print r.timestamp
|
||||
self.assertEqual( r.lastmod, '2012-03-14T00:00:00Z' )
|
||||
r.lastmod='2012-03-14T18:37:36Z'
|
||||
#print r.timestamp
|
||||
self.assertEqual( r.lastmod, '2012-03-14T18:37:36Z' )
|
||||
r.lastmod = '2012-03-14'
|
||||
self.assertEqual(r.lastmod, '2012-03-14T00:00:00Z')
|
||||
r.lastmod = '2012-03-14T00:00:00+00:00'
|
||||
# print r.timestamp
|
||||
self.assertEqual(r.lastmod, '2012-03-14T00:00:00Z')
|
||||
r.lastmod = '2012-03-14T00:00:00-00:00'
|
||||
# print r.timestamp
|
||||
self.assertEqual(r.lastmod, '2012-03-14T00:00:00Z')
|
||||
r.lastmod = '2012-03-14T18:37:36Z'
|
||||
# print r.timestamp
|
||||
self.assertEqual(r.lastmod, '2012-03-14T18:37:36Z')
|
||||
|
||||
def test06_str(self):
|
||||
r1 = Resource('abc',lastmod='2012-01-01')
|
||||
self.assertTrue( re.match( r"\[ abc \| 2012-01-01T", str(r1) ) )
|
||||
r1 = Resource('abc', lastmod='2012-01-01')
|
||||
self.assertTrue(re.match(r"\[ abc \| 2012-01-01T", str(r1)))
|
||||
|
||||
def test07_repr(self):
|
||||
r1 = Resource('def',lastmod='2012-01-01')
|
||||
self.assertEqual( repr(r1), "{'uri': 'def', 'timestamp': 1325376000}" )
|
||||
r1 = Resource('def', lastmod='2012-01-01')
|
||||
self.assertEqual(repr(r1), "{'uri': 'def', 'timestamp': 1325376000}")
|
||||
|
||||
def test08_multiple_hashes(self):
|
||||
r1 = Resource('abcd')
|
||||
r1.md5 = "some_md5"
|
||||
r1.sha1 = "some_sha1"
|
||||
r1.sha256 = "some_sha256"
|
||||
self.assertEqual( r1.md5, "some_md5" )
|
||||
self.assertEqual( r1.sha1, "some_sha1" )
|
||||
self.assertEqual( r1.sha256, "some_sha256" )
|
||||
self.assertEqual( r1.hash, "md5:some_md5 sha-1:some_sha1 sha-256:some_sha256" )
|
||||
self.assertEqual(r1.md5, "some_md5")
|
||||
self.assertEqual(r1.sha1, "some_sha1")
|
||||
self.assertEqual(r1.sha256, "some_sha256")
|
||||
self.assertEqual(
|
||||
r1.hash, "md5:some_md5 sha-1:some_sha1 sha-256:some_sha256")
|
||||
r2 = Resource('def')
|
||||
r2.hash = "md5:ddd"
|
||||
self.assertEqual( r2.md5, 'ddd' )
|
||||
self.assertEqual( r2.sha1, None )
|
||||
self.assertEqual(r2.md5, 'ddd')
|
||||
self.assertEqual(r2.sha1, None)
|
||||
r2.hash = "sha-1:eee"
|
||||
self.assertEqual( r2.md5, None )
|
||||
self.assertEqual( r2.sha1, 'eee' )
|
||||
self.assertEqual(r2.md5, None)
|
||||
self.assertEqual(r2.sha1, 'eee')
|
||||
r2.hash = "md5:fff sha-1:eee sha-256:ggg"
|
||||
self.assertEqual( r2.md5, 'fff' )
|
||||
self.assertEqual( r2.sha1, 'eee' )
|
||||
self.assertEqual( r2.sha256, 'ggg' )
|
||||
self.assertEqual(r2.md5, 'fff')
|
||||
self.assertEqual(r2.sha1, 'eee')
|
||||
self.assertEqual(r2.sha256, 'ggg')
|
||||
|
||||
def test09_changetypeerror(self):
|
||||
r1 = Resource('a')
|
||||
self.assertEqual( r1.change, None )
|
||||
self.assertEqual(r1.change, None)
|
||||
r1.change = 'deleted'
|
||||
self.assertEqual( r1.change, 'deleted' )
|
||||
self.assertRaises( ChangeTypeError, Resource, 'a', change="bad" )
|
||||
self.assertEqual(r1.change, 'deleted')
|
||||
self.assertRaises(ChangeTypeError, Resource, 'a', change="bad")
|
||||
# disable checking
|
||||
Resource.CHANGE_TYPES = False
|
||||
r1 = Resource( 'a', change="bad" )
|
||||
self.assertEqual( r1.change, 'bad' )
|
||||
r1 = Resource('a', change="bad")
|
||||
self.assertEqual(r1.change, 'bad')
|
||||
|
||||
def test10_md_at_roundtrips(self):
|
||||
r = Resource('a')
|
||||
r.md_at='2013-03-14'
|
||||
self.assertEqual( r.md_at, '2013-03-14T00:00:00Z' )
|
||||
r.md_at='2013-03-14T00:00:00+00:00'
|
||||
self.assertEqual( r.md_at, '2013-03-14T00:00:00Z' )
|
||||
r.md_at='2013-03-14T00:00:00-00:00'
|
||||
self.assertEqual( r.md_at, '2013-03-14T00:00:00Z' )
|
||||
r.md_at='2013-03-14T18:37:36Z'
|
||||
self.assertEqual( r.md_at, '2013-03-14T18:37:36Z' )
|
||||
r.md_at = '2013-03-14'
|
||||
self.assertEqual(r.md_at, '2013-03-14T00:00:00Z')
|
||||
r.md_at = '2013-03-14T00:00:00+00:00'
|
||||
self.assertEqual(r.md_at, '2013-03-14T00:00:00Z')
|
||||
r.md_at = '2013-03-14T00:00:00-00:00'
|
||||
self.assertEqual(r.md_at, '2013-03-14T00:00:00Z')
|
||||
r.md_at = '2013-03-14T18:37:36Z'
|
||||
self.assertEqual(r.md_at, '2013-03-14T18:37:36Z')
|
||||
|
||||
def test11_md_completed_roundtrips(self):
|
||||
r = Resource('a')
|
||||
r.md_completed='2013-04-14'
|
||||
self.assertEqual( r.md_completed, '2013-04-14T00:00:00Z' )
|
||||
r.md_completed='2013-04-14T00:00:00+00:00'
|
||||
self.assertEqual( r.md_completed, '2013-04-14T00:00:00Z' )
|
||||
r.md_completed='2013-04-14T00:00:00-00:00'
|
||||
self.assertEqual( r.md_completed, '2013-04-14T00:00:00Z' )
|
||||
r.md_completed='2013-04-14T18:37:36Z'
|
||||
self.assertEqual( r.md_completed, '2013-04-14T18:37:36Z' )
|
||||
r.md_completed = '2013-04-14'
|
||||
self.assertEqual(r.md_completed, '2013-04-14T00:00:00Z')
|
||||
r.md_completed = '2013-04-14T00:00:00+00:00'
|
||||
self.assertEqual(r.md_completed, '2013-04-14T00:00:00Z')
|
||||
r.md_completed = '2013-04-14T00:00:00-00:00'
|
||||
self.assertEqual(r.md_completed, '2013-04-14T00:00:00Z')
|
||||
r.md_completed = '2013-04-14T18:37:36Z'
|
||||
self.assertEqual(r.md_completed, '2013-04-14T18:37:36Z')
|
||||
|
||||
def test12_timevalues(self):
|
||||
r = Resource(uri='tv',
|
||||
@ -159,19 +164,19 @@ class TestResource(unittest.TestCase):
|
||||
md_completed="2000-01-03",
|
||||
md_from="2000-01-04",
|
||||
md_until="2000-01-05")
|
||||
self.assertEqual( r.lastmod, '2000-01-01T00:00:00Z' )
|
||||
self.assertEqual( r.md_at, '2000-01-02T00:00:00Z' )
|
||||
self.assertEqual( r.md_completed, '2000-01-03T00:00:00Z' )
|
||||
self.assertEqual( r.md_from, '2000-01-04T00:00:00Z' )
|
||||
self.assertEqual( r.md_until, '2000-01-05T00:00:00Z' )
|
||||
self.assertEqual(r.lastmod, '2000-01-01T00:00:00Z')
|
||||
self.assertEqual(r.md_at, '2000-01-02T00:00:00Z')
|
||||
self.assertEqual(r.md_completed, '2000-01-03T00:00:00Z')
|
||||
self.assertEqual(r.md_from, '2000-01-04T00:00:00Z')
|
||||
self.assertEqual(r.md_until, '2000-01-05T00:00:00Z')
|
||||
|
||||
def test13_mime_type(self):
|
||||
r = Resource(uri='tv1', mime_type='text/plain')
|
||||
self.assertEqual( r.mime_type, 'text/plain' )
|
||||
self.assertEqual(r.mime_type, 'text/plain')
|
||||
r.mime_type = None
|
||||
self.assertEqual( r.mime_type, None )
|
||||
self.assertEqual(r.mime_type, None)
|
||||
r = Resource(uri='tv2')
|
||||
self.assertEqual( r.mime_type, None )
|
||||
self.assertEqual(r.mime_type, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestResource)
|
||||
|
||||
@ -2,118 +2,120 @@ import unittest
|
||||
from resync.resource import Resource
|
||||
from resync.resource_container import ResourceContainer
|
||||
|
||||
|
||||
class TestResourceContainer(unittest.TestCase):
|
||||
|
||||
def test01_create_and_add(self):
|
||||
rc = ResourceContainer( resources=[] )
|
||||
self.assertEqual( len(rc.resources), 0, "empty" )
|
||||
rc.resources.append( Resource('a',timestamp=1) )
|
||||
rc.resources.append( Resource('b',timestamp=2) )
|
||||
self.assertEqual( len(rc.resources), 2, "two resources" )
|
||||
rc = ResourceContainer(resources=[])
|
||||
self.assertEqual(len(rc.resources), 0, "empty")
|
||||
rc.resources.append(Resource('a', timestamp=1))
|
||||
rc.resources.append(Resource('b', timestamp=2))
|
||||
self.assertEqual(len(rc.resources), 2, "two resources")
|
||||
|
||||
def test02_iter(self):
|
||||
rc = ResourceContainer( resources=[] )
|
||||
rc.resources.append( Resource('a',timestamp=1) )
|
||||
rc.resources.append( Resource('b',timestamp=2) )
|
||||
rc.resources.append( Resource('c',timestamp=3) )
|
||||
rc.resources.append( Resource('d',timestamp=4) )
|
||||
resources=[]
|
||||
rc = ResourceContainer(resources=[])
|
||||
rc.resources.append(Resource('a', timestamp=1))
|
||||
rc.resources.append(Resource('b', timestamp=2))
|
||||
rc.resources.append(Resource('c', timestamp=3))
|
||||
rc.resources.append(Resource('d', timestamp=4))
|
||||
resources = []
|
||||
for r in rc:
|
||||
resources.append(r)
|
||||
self.assertEqual(len(resources), 4)
|
||||
self.assertEqual( resources[0].uri, 'a')
|
||||
self.assertEqual( resources[3].uri, 'd')
|
||||
self.assertEqual(resources[0].uri, 'a')
|
||||
self.assertEqual(resources[3].uri, 'd')
|
||||
|
||||
def test03_prune_before(self):
|
||||
rc = ResourceContainer()
|
||||
rc.resources.append( Resource('a',timestamp=1) )
|
||||
rc.resources.append( Resource('b',timestamp=2) )
|
||||
rc.resources.append( Resource('c',timestamp=3) )
|
||||
rc.resources.append( Resource('d',timestamp=4) )
|
||||
rc.resources.append(Resource('a', timestamp=1))
|
||||
rc.resources.append(Resource('b', timestamp=2))
|
||||
rc.resources.append(Resource('c', timestamp=3))
|
||||
rc.resources.append(Resource('d', timestamp=4))
|
||||
rc.prune_before(3)
|
||||
self.assertEqual( len(rc.resources), 2 )
|
||||
self.assertEqual(len(rc.resources), 2)
|
||||
i = iter(rc)
|
||||
self.assertEqual( next(i).uri, 'c' )
|
||||
self.assertEqual( next(i).uri, 'd' )
|
||||
self.assertEqual(next(i).uri, 'c')
|
||||
self.assertEqual(next(i).uri, 'd')
|
||||
# put some more back out of order
|
||||
rc.resources.append( Resource('a',timestamp=1) )
|
||||
rc.resources.append( Resource('b',timestamp=2) )
|
||||
rc.resources.append( Resource('e',timestamp=1000) )
|
||||
rc.resources.append(Resource('a', timestamp=1))
|
||||
rc.resources.append(Resource('b', timestamp=2))
|
||||
rc.resources.append(Resource('e', timestamp=1000))
|
||||
rc.prune_before(3.5)
|
||||
self.assertEqual( len(rc.resources), 2 )
|
||||
self.assertEqual(len(rc.resources), 2)
|
||||
i = iter(rc)
|
||||
self.assertEqual( next(i).uri, 'd' )
|
||||
self.assertEqual( next(i).uri, 'e' )
|
||||
self.assertEqual(next(i).uri, 'd')
|
||||
self.assertEqual(next(i).uri, 'e')
|
||||
|
||||
def test04_prune_dupes(self):
|
||||
rc = ResourceContainer()
|
||||
rc.resources.append( Resource('a',timestamp=1, change='created') )
|
||||
rc.resources.append( Resource('b',timestamp=2, change='created') )
|
||||
rc.resources.append( Resource('c',timestamp=3, change='updated') )
|
||||
rc.resources.append( Resource('d',timestamp=4, change='deleted') )
|
||||
rc.resources.append( Resource('a',timestamp=4, change='updated') )
|
||||
rc.resources.append( Resource('b',timestamp=4, change='deleted') )
|
||||
rc.resources.append( Resource('b',timestamp=5, change='created') )
|
||||
rc.resources.append( Resource('b',timestamp=6, change='deleted') )
|
||||
rc.resources.append( Resource('c',timestamp=7, change='deleted') )
|
||||
rc.resources.append( Resource('d',timestamp=8, change='created') )
|
||||
rc.resources.append(Resource('a', timestamp=1, change='created'))
|
||||
rc.resources.append(Resource('b', timestamp=2, change='created'))
|
||||
rc.resources.append(Resource('c', timestamp=3, change='updated'))
|
||||
rc.resources.append(Resource('d', timestamp=4, change='deleted'))
|
||||
rc.resources.append(Resource('a', timestamp=4, change='updated'))
|
||||
rc.resources.append(Resource('b', timestamp=4, change='deleted'))
|
||||
rc.resources.append(Resource('b', timestamp=5, change='created'))
|
||||
rc.resources.append(Resource('b', timestamp=6, change='deleted'))
|
||||
rc.resources.append(Resource('c', timestamp=7, change='deleted'))
|
||||
rc.resources.append(Resource('d', timestamp=8, change='created'))
|
||||
rc.prune_dupes()
|
||||
self.assertEqual( len(rc.resources), 3 )
|
||||
self.assertEqual(len(rc.resources), 3)
|
||||
i = iter(rc)
|
||||
i1 = next(i)
|
||||
self.assertEqual( i1.uri, 'a' )
|
||||
self.assertEqual( i1.change, 'updated' )
|
||||
self.assertEqual(i1.uri, 'a')
|
||||
self.assertEqual(i1.change, 'updated')
|
||||
i2 = next(i)
|
||||
self.assertEqual( i2.uri, 'c' )
|
||||
self.assertEqual( i2.change, 'deleted' )
|
||||
self.assertEqual(i2.uri, 'c')
|
||||
self.assertEqual(i2.change, 'deleted')
|
||||
i3 = next(i)
|
||||
self.assertEqual( i3.uri, 'd' )
|
||||
self.assertEqual( i3.change, 'created' )
|
||||
self.assertEqual(i3.uri, 'd')
|
||||
self.assertEqual(i3.change, 'created')
|
||||
|
||||
def test05_from_until(self):
|
||||
rc = ResourceContainer()
|
||||
# via convenience methods
|
||||
self.assertEqual( rc.md_from, None )
|
||||
self.assertEqual( rc.md_until, None )
|
||||
self.assertEqual(rc.md_from, None)
|
||||
self.assertEqual(rc.md_until, None)
|
||||
rc.md_from = "ftime"
|
||||
rc.md_until = "utime"
|
||||
self.assertEqual( rc.md_from, "ftime" )
|
||||
self.assertEqual( rc.md_until, "utime" )
|
||||
self.assertEqual(rc.md_from, "ftime")
|
||||
self.assertEqual(rc.md_until, "utime")
|
||||
# via underlying dict
|
||||
rc.md['md_from'] = "ftime2"
|
||||
rc.md['md_until'] = "utime2too"
|
||||
self.assertEqual( rc.md_from, "ftime2" )
|
||||
self.assertEqual( rc.md_until, "utime2too" )
|
||||
self.assertEqual(rc.md_from, "ftime2")
|
||||
self.assertEqual(rc.md_until, "utime2too")
|
||||
|
||||
def test06_source_description(self):
|
||||
# up links used to point to the source description
|
||||
rc = ResourceContainer()
|
||||
# via convenience methods
|
||||
self.assertEqual( rc.up, None )
|
||||
# via convenience methods
|
||||
self.assertEqual(rc.up, None)
|
||||
rc.up = "well-known"
|
||||
self.assertEqual( rc.up, "well-known" )
|
||||
self.assertEqual(rc.up, "well-known")
|
||||
rc.up = "well-known2"
|
||||
self.assertEqual( rc.up, "well-known2" )
|
||||
self.assertEqual(rc.up, "well-known2")
|
||||
# via link
|
||||
link = rc.link("up")
|
||||
self.assertEqual( link['href'], "well-known2" )
|
||||
self.assertEqual(link['href'], "well-known2")
|
||||
link['href'] = "wk3"
|
||||
self.assertEqual( rc.up, "wk3" )
|
||||
self.assertEqual(rc.up, "wk3")
|
||||
|
||||
def test07_describedby(self):
|
||||
rc = ResourceContainer()
|
||||
# via convenience methods
|
||||
self.assertEqual( rc.describedby, None )
|
||||
# via convenience methods
|
||||
self.assertEqual(rc.describedby, None)
|
||||
rc.describedby = "db_uri"
|
||||
self.assertEqual( rc.describedby, "db_uri" )
|
||||
self.assertEqual(rc.describedby, "db_uri")
|
||||
|
||||
def test08_up(self):
|
||||
rc = ResourceContainer()
|
||||
# via convenience methods
|
||||
self.assertEqual( rc.up, None )
|
||||
# via convenience methods
|
||||
self.assertEqual(rc.up, None)
|
||||
rc.up = "up_uri"
|
||||
self.assertEqual( rc.up, "up_uri" )
|
||||
self.assertEqual(rc.up, "up_uri")
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestResourceContainer)
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(
|
||||
TestResourceContainer)
|
||||
unittest.TextTestRunner().run(suite)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
import re
|
||||
|
||||
@ -11,55 +11,60 @@ from resync.resource import Resource
|
||||
from resync.resource_dump_manifest import ResourceDumpManifest
|
||||
from resync.sitemap import SitemapParseError
|
||||
|
||||
|
||||
class TestResourceDumpManifest(unittest.TestCase):
|
||||
|
||||
def test01_as_xml(self):
|
||||
rdm = ResourceDumpManifest()
|
||||
rdm.add( Resource('a.zip',timestamp=1) )
|
||||
rdm.add( Resource('b.zip',timestamp=2) )
|
||||
rdm.add(Resource('a.zip', timestamp=1))
|
||||
rdm.add(Resource('b.zip', timestamp=2))
|
||||
xml = rdm.as_xml()
|
||||
self.assertTrue( re.search(r'<rs:md .*capability="resourcedump-manifest"', xml), 'XML has capability' )
|
||||
self.assertTrue( re.search(r'<url><loc>a.zip</loc><lastmod>1970-01-01T00:00:01Z</lastmod></url>', xml), 'XML has resource a' )
|
||||
self.assertTrue(re.search(
|
||||
r'<rs:md .*capability="resourcedump-manifest"', xml), 'XML has capability')
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>a.zip</loc><lastmod>1970-01-01T00:00:01Z</lastmod></url>', xml), 'XML has resource a')
|
||||
|
||||
def test10_parse(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md at="2013-08-08" capability="resourcedump-manifest"/>\
|
||||
<url><loc>http://example.com/res1</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="12" path="/res1" /></url>\
|
||||
<url><loc>http://example.com/res2</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="32" path="/res2"/></url>\
|
||||
</urlset>'
|
||||
rdm=ResourceDumpManifest()
|
||||
rdm = ResourceDumpManifest()
|
||||
rdm.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(rdm.resources), 2, 'got 2 resource dumps')
|
||||
self.assertEqual( rdm.capability, 'resourcedump-manifest', 'capability set' )
|
||||
self.assertEqual( rdm.md_at, '2013-08-08' )
|
||||
self.assertTrue( 'http://example.com/res1' in rdm.resources )
|
||||
self.assertTrue( rdm.resources['http://example.com/res1'].length, 12 )
|
||||
self.assertTrue( rdm.resources['http://example.com/res1'].path, '/res1' )
|
||||
self.assertTrue( 'http://example.com/res2' in rdm.resources )
|
||||
self.assertTrue( rdm.resources['http://example.com/res2'].path, '/res2' )
|
||||
self.assertEqual(len(rdm.resources), 2, 'got 2 resource dumps')
|
||||
self.assertEqual(
|
||||
rdm.capability, 'resourcedump-manifest', 'capability set')
|
||||
self.assertEqual(rdm.md_at, '2013-08-08')
|
||||
self.assertTrue('http://example.com/res1' in rdm.resources)
|
||||
self.assertTrue(rdm.resources['http://example.com/res1'].length, 12)
|
||||
self.assertTrue(rdm.resources['http://example.com/res1'].path, '/res1')
|
||||
self.assertTrue('http://example.com/res2' in rdm.resources)
|
||||
self.assertTrue(rdm.resources['http://example.com/res2'].path, '/res2')
|
||||
|
||||
def test11_parse_no_capability(self):
|
||||
# For a resource dump this should be an error
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md from="2013-01-01"/>\
|
||||
<url><loc>http://example.com/res1</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="12" path="/res1" /></url>\
|
||||
<url><loc>http://example.com/res2</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="32" path="/res2"/></url>\
|
||||
</urlset>'
|
||||
rdm=ResourceDumpManifest()
|
||||
self.assertRaises( SitemapParseError, rdm.parse, fh=io.StringIO(xml) )
|
||||
rdm = ResourceDumpManifest()
|
||||
self.assertRaises(SitemapParseError, rdm.parse, fh=io.StringIO(xml))
|
||||
|
||||
def test12_parse_bad_capability(self):
|
||||
# the <rs:md capability="bad_capability".. should give error
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="bad_capability" from="2013-01-01"/>\
|
||||
<url><loc>http://example.com/a.zip</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="12" /></url>\
|
||||
</urlset>'
|
||||
rdm=ResourceDumpManifest()
|
||||
self.assertRaises( SitemapParseError, rdm.parse, fh=io.StringIO(xml) )
|
||||
rdm = ResourceDumpManifest()
|
||||
self.assertRaises(SitemapParseError, rdm.parse, fh=io.StringIO(xml))
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestResourceDumpManifest)
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(
|
||||
TestResourceDumpManifest)
|
||||
unittest.TextTestRunner().run(suite)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
import re
|
||||
|
||||
@ -11,169 +11,173 @@ from resync.resource import Resource
|
||||
from resync.resource_list import ResourceList, ResourceListDupeError
|
||||
from resync.sitemap import SitemapParseError
|
||||
|
||||
|
||||
class TestResourceList(unittest.TestCase):
|
||||
|
||||
def test01_same(self):
|
||||
src = ResourceList()
|
||||
src.add( Resource('a',timestamp=1) )
|
||||
src.add( Resource('b',timestamp=2) )
|
||||
src.add(Resource('a', timestamp=1))
|
||||
src.add(Resource('b', timestamp=2))
|
||||
dst = ResourceList()
|
||||
dst.add( Resource('a',timestamp=1) )
|
||||
dst.add( Resource('b',timestamp=2) )
|
||||
( same, changed, deleted, added ) = dst.compare(src)
|
||||
self.assertEqual( len(same), 2, "2 things unchanged" )
|
||||
dst.add(Resource('a', timestamp=1))
|
||||
dst.add(Resource('b', timestamp=2))
|
||||
(same, changed, deleted, added) = dst.compare(src)
|
||||
self.assertEqual(len(same), 2, "2 things unchanged")
|
||||
i = iter(same)
|
||||
self.assertEqual( next(i).uri, 'a', "first was a" )
|
||||
self.assertEqual( next(i).uri, 'b', "second was b" )
|
||||
self.assertEqual( len(changed), 0, "nothing changed" )
|
||||
self.assertEqual( len(deleted), 0, "nothing deleted" )
|
||||
self.assertEqual( len(added), 0, "nothing added" )
|
||||
self.assertEqual(next(i).uri, 'a', "first was a")
|
||||
self.assertEqual(next(i).uri, 'b', "second was b")
|
||||
self.assertEqual(len(changed), 0, "nothing changed")
|
||||
self.assertEqual(len(deleted), 0, "nothing deleted")
|
||||
self.assertEqual(len(added), 0, "nothing added")
|
||||
|
||||
def test02_changed(self):
|
||||
src = ResourceList()
|
||||
src.add( Resource('a',timestamp=1) )
|
||||
src.add( Resource('b',timestamp=2) )
|
||||
src.add(Resource('a', timestamp=1))
|
||||
src.add(Resource('b', timestamp=2))
|
||||
dst = ResourceList()
|
||||
dst.add( Resource('a',timestamp=3) )
|
||||
dst.add( Resource('b',timestamp=4) )
|
||||
( same, changed, deleted, added ) = dst.compare(src)
|
||||
self.assertEqual( len(same), 0, "0 things unchanged" )
|
||||
self.assertEqual( len(changed), 2, "2 things changed" )
|
||||
dst.add(Resource('a', timestamp=3))
|
||||
dst.add(Resource('b', timestamp=4))
|
||||
(same, changed, deleted, added) = dst.compare(src)
|
||||
self.assertEqual(len(same), 0, "0 things unchanged")
|
||||
self.assertEqual(len(changed), 2, "2 things changed")
|
||||
i = iter(changed)
|
||||
self.assertEqual( next(i).uri, 'a', "first was a" )
|
||||
self.assertEqual( next(i).uri, 'b', "second was b" )
|
||||
self.assertEqual( len(deleted), 0, "nothing deleted" )
|
||||
self.assertEqual( len(added), 0, "nothing added" )
|
||||
self.assertEqual(next(i).uri, 'a', "first was a")
|
||||
self.assertEqual(next(i).uri, 'b', "second was b")
|
||||
self.assertEqual(len(deleted), 0, "nothing deleted")
|
||||
self.assertEqual(len(added), 0, "nothing added")
|
||||
|
||||
def test03_deleted(self):
|
||||
src = ResourceList()
|
||||
src.add( Resource('a',timestamp=1) )
|
||||
src.add( Resource('b',timestamp=2) )
|
||||
src.add(Resource('a', timestamp=1))
|
||||
src.add(Resource('b', timestamp=2))
|
||||
dst = ResourceList()
|
||||
dst.add( Resource('a',timestamp=1) )
|
||||
dst.add( Resource('b',timestamp=2) )
|
||||
dst.add( Resource('c',timestamp=3) )
|
||||
dst.add( Resource('d',timestamp=4) )
|
||||
( same, changed, deleted, added ) = dst.compare(src)
|
||||
self.assertEqual( len(same), 2, "2 things unchanged" )
|
||||
self.assertEqual( len(changed), 0, "nothing changed" )
|
||||
self.assertEqual( len(deleted), 2, "c and d deleted" )
|
||||
dst.add(Resource('a', timestamp=1))
|
||||
dst.add(Resource('b', timestamp=2))
|
||||
dst.add(Resource('c', timestamp=3))
|
||||
dst.add(Resource('d', timestamp=4))
|
||||
(same, changed, deleted, added) = dst.compare(src)
|
||||
self.assertEqual(len(same), 2, "2 things unchanged")
|
||||
self.assertEqual(len(changed), 0, "nothing changed")
|
||||
self.assertEqual(len(deleted), 2, "c and d deleted")
|
||||
i = iter(deleted)
|
||||
self.assertEqual( next(i).uri, 'c', "first was c" )
|
||||
self.assertEqual( next(i).uri, 'd', "second was d" )
|
||||
self.assertEqual( len(added), 0, "nothing added" )
|
||||
self.assertEqual(next(i).uri, 'c', "first was c")
|
||||
self.assertEqual(next(i).uri, 'd', "second was d")
|
||||
self.assertEqual(len(added), 0, "nothing added")
|
||||
|
||||
def test04_added(self):
|
||||
src = ResourceList()
|
||||
src.add( Resource('a',timestamp=1) )
|
||||
src.add( Resource('b',timestamp=2) )
|
||||
src.add( Resource('c',timestamp=3) )
|
||||
src.add( Resource('d',timestamp=4) )
|
||||
src.add(Resource('a', timestamp=1))
|
||||
src.add(Resource('b', timestamp=2))
|
||||
src.add(Resource('c', timestamp=3))
|
||||
src.add(Resource('d', timestamp=4))
|
||||
dst = ResourceList()
|
||||
dst.add( Resource('a',timestamp=1) )
|
||||
dst.add( Resource('c',timestamp=3) )
|
||||
( same, changed, deleted, added ) = dst.compare(src)
|
||||
self.assertEqual( len(same), 2, "2 things unchanged" )
|
||||
self.assertEqual( len(changed), 0, "nothing changed" )
|
||||
self.assertEqual( len(deleted), 0, "nothing deleted" )
|
||||
self.assertEqual( len(added), 2, "b and d added" )
|
||||
dst.add(Resource('a', timestamp=1))
|
||||
dst.add(Resource('c', timestamp=3))
|
||||
(same, changed, deleted, added) = dst.compare(src)
|
||||
self.assertEqual(len(same), 2, "2 things unchanged")
|
||||
self.assertEqual(len(changed), 0, "nothing changed")
|
||||
self.assertEqual(len(deleted), 0, "nothing deleted")
|
||||
self.assertEqual(len(added), 2, "b and d added")
|
||||
i = iter(added)
|
||||
self.assertEqual( next(i).uri, 'b', "first was b" )
|
||||
self.assertEqual( next(i).uri, 'd', "second was d" )
|
||||
self.assertEqual(next(i).uri, 'b', "first was b")
|
||||
self.assertEqual(next(i).uri, 'd', "second was d")
|
||||
|
||||
def test05_add(self):
|
||||
r1 = Resource(uri='a',length=1)
|
||||
r2 = Resource(uri='b',length=2)
|
||||
r1 = Resource(uri='a', length=1)
|
||||
r2 = Resource(uri='b', length=2)
|
||||
i = ResourceList()
|
||||
i.add(r1)
|
||||
self.assertRaises( ResourceListDupeError, i.add, r1)
|
||||
self.assertRaises(ResourceListDupeError, i.add, r1)
|
||||
i.add(r2)
|
||||
self.assertRaises( ResourceListDupeError, i.add, r2)
|
||||
self.assertRaises(ResourceListDupeError, i.add, r2)
|
||||
# allow dupes
|
||||
r1d = Resource(uri='a',length=10)
|
||||
i.add(r1d,replace=True)
|
||||
self.assertEqual( len(i), 2 )
|
||||
self.assertEqual( i.resources['a'].length, 10 )
|
||||
r1d = Resource(uri='a', length=10)
|
||||
i.add(r1d, replace=True)
|
||||
self.assertEqual(len(i), 2)
|
||||
self.assertEqual(i.resources['a'].length, 10)
|
||||
|
||||
def test06_add_iterable(self):
|
||||
r1 = Resource(uri='a',length=1)
|
||||
r2 = Resource(uri='b',length=2)
|
||||
r1 = Resource(uri='a', length=1)
|
||||
r2 = Resource(uri='b', length=2)
|
||||
i = ResourceList()
|
||||
i.add( [r1,r2] )
|
||||
self.assertRaises( ResourceListDupeError, i.add, r1)
|
||||
self.assertRaises( ResourceListDupeError, i.add, r2)
|
||||
i.add([r1, r2])
|
||||
self.assertRaises(ResourceListDupeError, i.add, r1)
|
||||
self.assertRaises(ResourceListDupeError, i.add, r2)
|
||||
# allow dupes
|
||||
r1d = Resource(uri='a',length=10)
|
||||
i.add( [r1d] ,replace=True)
|
||||
self.assertEqual( len(i), 2 )
|
||||
self.assertEqual( i.resources['a'].length, 10 )
|
||||
r1d = Resource(uri='a', length=10)
|
||||
i.add([r1d], replace=True)
|
||||
self.assertEqual(len(i), 2)
|
||||
self.assertEqual(i.resources['a'].length, 10)
|
||||
|
||||
def test07_has_md5(self):
|
||||
r1 = Resource(uri='a')
|
||||
r2 = Resource(uri='b')
|
||||
i = ResourceList()
|
||||
self.assertFalse( i.has_md5() )
|
||||
self.assertFalse(i.has_md5())
|
||||
i.add(r1)
|
||||
i.add(r2)
|
||||
self.assertFalse( i.has_md5() )
|
||||
r1.md5="aabbcc"
|
||||
self.assertTrue( i.has_md5() )
|
||||
self.assertFalse(i.has_md5())
|
||||
r1.md5 = "aabbcc"
|
||||
self.assertTrue(i.has_md5())
|
||||
|
||||
def test08_iter(self):
|
||||
i = ResourceList()
|
||||
i.add( Resource('a',timestamp=1) )
|
||||
i.add( Resource('b',timestamp=2) )
|
||||
i.add( Resource('c',timestamp=3) )
|
||||
i.add( Resource('d',timestamp=4) )
|
||||
resources=[]
|
||||
i.add(Resource('a', timestamp=1))
|
||||
i.add(Resource('b', timestamp=2))
|
||||
i.add(Resource('c', timestamp=3))
|
||||
i.add(Resource('d', timestamp=4))
|
||||
resources = []
|
||||
for r in i:
|
||||
resources.append(r)
|
||||
self.assertEqual(len(resources), 4)
|
||||
self.assertEqual( resources[0].uri, 'a')
|
||||
self.assertEqual( resources[3].uri, 'd')
|
||||
self.assertEqual(resources[0].uri, 'a')
|
||||
self.assertEqual(resources[3].uri, 'd')
|
||||
|
||||
def test20_as_xml(self):
|
||||
rl = ResourceList()
|
||||
rl.add( Resource('a',timestamp=1) )
|
||||
rl.add( Resource('b',timestamp=2) )
|
||||
rl.add(Resource('a', timestamp=1))
|
||||
rl.add(Resource('b', timestamp=2))
|
||||
xml = rl.as_xml()
|
||||
self.assertTrue( re.search(r'<rs:md .*capability="resourcelist"', xml), 'XML has capability' )
|
||||
self.assertTrue( re.search(r'<url><loc>a</loc><lastmod>1970-01-01T00:00:01Z</lastmod></url>', xml), 'XML has resource a' )
|
||||
self.assertTrue(
|
||||
re.search(r'<rs:md .*capability="resourcelist"', xml), 'XML has capability')
|
||||
self.assertTrue(re.search(
|
||||
r'<url><loc>a</loc><lastmod>1970-01-01T00:00:01Z</lastmod></url>', xml), 'XML has resource a')
|
||||
|
||||
def test30_parse(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md at="2013-08-07" capability="resourcelist" completed="2013-08-08" />\
|
||||
<url><loc>/tmp/rs_test/src/file_a</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md change="updated" length="12" /></url>\
|
||||
<url><loc>/tmp/rs_test/src/file_b</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="32" /></url>\
|
||||
</urlset>'
|
||||
rl=ResourceList()
|
||||
rl = ResourceList()
|
||||
rl.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(rl.resources), 2, 'got 2 resources')
|
||||
self.assertEqual( rl.md['capability'], 'resourcelist', 'capability set' )
|
||||
self.assertEqual( rl.md_at, '2013-08-07' )
|
||||
self.assertEqual( rl.md_completed, '2013-08-08' )
|
||||
self.assertEqual(len(rl.resources), 2, 'got 2 resources')
|
||||
self.assertEqual(rl.md['capability'], 'resourcelist', 'capability set')
|
||||
self.assertEqual(rl.md_at, '2013-08-07')
|
||||
self.assertEqual(rl.md_completed, '2013-08-08')
|
||||
|
||||
def test31_parse_no_capability(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\
|
||||
<url><loc>http://example.com/res1</loc><lastmod>2012-03-14T18:37:36Z</lastmod></url>\
|
||||
</urlset>'
|
||||
rl=ResourceList()
|
||||
rl = ResourceList()
|
||||
rl.parse(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(rl.resources), 1, 'got 1 resource')
|
||||
self.assertEqual( rl.md['capability'], 'resourcelist', 'capability set by reading routine' )
|
||||
self.assertFalse( 'from' in rl.md )
|
||||
self.assertEqual(len(rl.resources), 1, 'got 1 resource')
|
||||
self.assertEqual(rl.md['capability'], 'resourcelist',
|
||||
'capability set by reading routine')
|
||||
self.assertFalse('from' in rl.md)
|
||||
|
||||
def test32_parse_bad_capability(self):
|
||||
# the <rs:md capability="bad_capability".. should give error
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="bad_capability" from="2013-01-01"/>\
|
||||
<url><loc>http://example.com/bad_res_1</loc><lastmod>2012-03-14T18:37:36Z</lastmod></url>\
|
||||
</urlset>'
|
||||
rl=ResourceList()
|
||||
self.assertRaises( SitemapParseError, rl.parse, fh=io.StringIO(xml) )
|
||||
rl = ResourceList()
|
||||
self.assertRaises(SitemapParseError, rl.parse, fh=io.StringIO(xml))
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestResourceList)
|
||||
|
||||
@ -5,6 +5,7 @@ import time
|
||||
from resync.resource_list_builder import ResourceListBuilder
|
||||
from resync.mapper import Mapper
|
||||
|
||||
|
||||
class TestResourceListBuilder(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@ -12,111 +13,113 @@ class TestResourceListBuilder(unittest.TestCase):
|
||||
# in UTC so no conversion issues.
|
||||
# Test case file_a: 1343236426 = 2012-07-25T17:13:46Z
|
||||
# Test case file_b: 1000000000 = 2001-09-09T01:46:40Z
|
||||
os.utime( "tests/testdata/dir1/file_a", (0, 1343236426 ) )
|
||||
os.utime( "tests/testdata/dir1/file_b", (0, 1000000000 ) )
|
||||
os.utime("tests/testdata/dir1/file_a", (0, 1343236426))
|
||||
os.utime("tests/testdata/dir1/file_b", (0, 1000000000))
|
||||
|
||||
def test01_simple_scan(self):
|
||||
rlb = ResourceListBuilder()
|
||||
rlb.mapper = Mapper(['http://example.org/t','tests/testdata/dir1'])
|
||||
rlb.mapper = Mapper(['http://example.org/t', 'tests/testdata/dir1'])
|
||||
rl = rlb.from_disk()
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertEqual(len(rl), 2)
|
||||
rli = iter(rl)
|
||||
r = next(rli)
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_a' )
|
||||
self.assertEqual( r.lastmod, '2012-07-25T17:13:46Z' )
|
||||
self.assertEqual( r.md5, None )
|
||||
self.assertEqual( r.length, 20 )
|
||||
self.assertEqual( r.path, None )
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_a')
|
||||
self.assertEqual(r.lastmod, '2012-07-25T17:13:46Z')
|
||||
self.assertEqual(r.md5, None)
|
||||
self.assertEqual(r.length, 20)
|
||||
self.assertEqual(r.path, None)
|
||||
r = next(rli)
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_b' )
|
||||
self.assertEqual( r.lastmod, '2001-09-09T01:46:40Z' )
|
||||
self.assertEqual( r.md5, None )
|
||||
self.assertEqual( r.length, 45 )
|
||||
self.assertEqual( r.path, None )
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_b')
|
||||
self.assertEqual(r.lastmod, '2001-09-09T01:46:40Z')
|
||||
self.assertEqual(r.md5, None)
|
||||
self.assertEqual(r.length, 45)
|
||||
self.assertEqual(r.path, None)
|
||||
# Make sure at and completed were set
|
||||
self.assertTrue( rl.md_at is not None )
|
||||
self.assertTrue( rl.md_completed is not None )
|
||||
self.assertTrue(rl.md_at is not None)
|
||||
self.assertTrue(rl.md_completed is not None)
|
||||
|
||||
def test02_no_length(self):
|
||||
rlb = ResourceListBuilder(set_length=False)
|
||||
rlb.mapper = Mapper(['http://example.org/t','tests/testdata/dir1'])
|
||||
rlb.mapper = Mapper(['http://example.org/t', 'tests/testdata/dir1'])
|
||||
rl = rlb.from_disk()
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertEqual(len(rl), 2)
|
||||
rli = iter(rl)
|
||||
r = next(rli)
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_a' )
|
||||
self.assertEqual( r.lastmod, '2012-07-25T17:13:46Z' )
|
||||
self.assertEqual( r.md5, None )
|
||||
self.assertEqual( r.length, None )
|
||||
self.assertEqual( r.path, None )
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_a')
|
||||
self.assertEqual(r.lastmod, '2012-07-25T17:13:46Z')
|
||||
self.assertEqual(r.md5, None)
|
||||
self.assertEqual(r.length, None)
|
||||
self.assertEqual(r.path, None)
|
||||
r = next(rli)
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_b' )
|
||||
self.assertEqual( r.lastmod, '2001-09-09T01:46:40Z' )
|
||||
self.assertEqual( r.md5, None )
|
||||
self.assertEqual( r.length, None )
|
||||
self.assertEqual( r.path, None )
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_b')
|
||||
self.assertEqual(r.lastmod, '2001-09-09T01:46:40Z')
|
||||
self.assertEqual(r.md5, None)
|
||||
self.assertEqual(r.length, None)
|
||||
self.assertEqual(r.path, None)
|
||||
|
||||
def test03_set_md5(self):
|
||||
rlb = ResourceListBuilder(set_md5=True)
|
||||
rlb.mapper = Mapper(['http://example.org/t','tests/testdata/dir1'])
|
||||
rlb.mapper = Mapper(['http://example.org/t', 'tests/testdata/dir1'])
|
||||
rl = rlb.from_disk()
|
||||
self.assertEqual( len(rl), 2 )
|
||||
self.assertEqual(len(rl), 2)
|
||||
rli = iter(rl)
|
||||
r = next(rli)
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_a' )
|
||||
self.assertEqual( r.lastmod, '2012-07-25T17:13:46Z' )
|
||||
self.assertEqual( r.md5, 'a/Jv1mYBtSjS4LR+qoft/Q==' )
|
||||
self.assertEqual( r.length, 20 )
|
||||
self.assertEqual( r.path, None )
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_a')
|
||||
self.assertEqual(r.lastmod, '2012-07-25T17:13:46Z')
|
||||
self.assertEqual(r.md5, 'a/Jv1mYBtSjS4LR+qoft/Q==')
|
||||
self.assertEqual(r.length, 20)
|
||||
self.assertEqual(r.path, None)
|
||||
r = next(rli)
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_b' )
|
||||
self.assertEqual( r.lastmod, '2001-09-09T01:46:40Z' )
|
||||
self.assertEqual( r.md5, 'RS5Uva4WJqxdbnvoGzneIQ==' )
|
||||
self.assertEqual( r.length, 45 )
|
||||
self.assertEqual( r.path, None )
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_b')
|
||||
self.assertEqual(r.lastmod, '2001-09-09T01:46:40Z')
|
||||
self.assertEqual(r.md5, 'RS5Uva4WJqxdbnvoGzneIQ==')
|
||||
self.assertEqual(r.length, 45)
|
||||
self.assertEqual(r.path, None)
|
||||
|
||||
def test04_data(self):
|
||||
rlb = ResourceListBuilder(set_path=True,set_md5=True)
|
||||
rlb.mapper = Mapper(['http://example.org/t','tests/testdata/dir1'])
|
||||
rlb = ResourceListBuilder(set_path=True, set_md5=True)
|
||||
rlb.mapper = Mapper(['http://example.org/t', 'tests/testdata/dir1'])
|
||||
rl = rlb.from_disk()
|
||||
self.assertEqual( len(rl), 2)
|
||||
self.assertEqual(len(rl), 2)
|
||||
r = rl.resources.get('http://example.org/t/file_a')
|
||||
self.assertTrue( r is not None )
|
||||
self.assertEqual( r.uri, 'http://example.org/t/file_a' )
|
||||
self.assertEqual( r.lastmod, '2012-07-25T17:13:46Z' )
|
||||
self.assertEqual( r.md5, 'a/Jv1mYBtSjS4LR+qoft/Q==' )
|
||||
self.assertEqual( r.path, 'tests/testdata/dir1/file_a' )
|
||||
self.assertTrue(r is not None)
|
||||
self.assertEqual(r.uri, 'http://example.org/t/file_a')
|
||||
self.assertEqual(r.lastmod, '2012-07-25T17:13:46Z')
|
||||
self.assertEqual(r.md5, 'a/Jv1mYBtSjS4LR+qoft/Q==')
|
||||
self.assertEqual(r.path, 'tests/testdata/dir1/file_a')
|
||||
|
||||
def test05_from_disk_paths(self):
|
||||
rlb = ResourceListBuilder()
|
||||
rlb.mapper = Mapper(['http://example.org/t','tests/testdata/dir1'])
|
||||
rlb.mapper = Mapper(['http://example.org/t', 'tests/testdata/dir1'])
|
||||
# no path, should get no resources
|
||||
rl = rlb.from_disk(paths=[])
|
||||
self.assertEqual( len(rl), 0)
|
||||
self.assertEqual(len(rl), 0)
|
||||
# full path, 2 resources
|
||||
rl = rlb.from_disk(paths=['tests/testdata/dir1'])
|
||||
self.assertEqual( len(rl), 2)
|
||||
self.assertEqual(len(rl), 2)
|
||||
# new object with mapper covering larger space of disk
|
||||
rlb = ResourceListBuilder(set_path=True)
|
||||
rlb.mapper = Mapper(['http://example.org/t','tests/testdata'])
|
||||
rlb.mapper = Mapper(['http://example.org/t', 'tests/testdata'])
|
||||
# same path with 2 resources
|
||||
rl = rlb.from_disk(paths=['tests/testdata/dir1'])
|
||||
self.assertEqual( len(rl), 2)
|
||||
self.assertEqual(len(rl), 2)
|
||||
# same path with 2 resources
|
||||
rl = rlb.from_disk(paths=['tests/testdata/dir1','tests/testdata/dir2'])
|
||||
self.assertEqual( len(rl), 3)
|
||||
rl = rlb.from_disk(
|
||||
paths=['tests/testdata/dir1', 'tests/testdata/dir2'])
|
||||
self.assertEqual(len(rl), 3)
|
||||
# path that is just a single file
|
||||
rl = rlb.from_disk(paths=['tests/testdata/dir1/file_a'])
|
||||
self.assertEqual( len(rl), 1)
|
||||
self.assertEqual(len(rl), 1)
|
||||
rli = iter(rl)
|
||||
r = next(rli)
|
||||
self.assertTrue( r is not None )
|
||||
self.assertEqual( r.uri, 'http://example.org/t/dir1/file_a' )
|
||||
self.assertEqual( r.lastmod, '2012-07-25T17:13:46Z' )
|
||||
self.assertEqual( r.md5, None )
|
||||
self.assertEqual( r.length, 20 )
|
||||
self.assertEqual( r.path, 'tests/testdata/dir1/file_a' )
|
||||
self.assertTrue(r is not None)
|
||||
self.assertEqual(r.uri, 'http://example.org/t/dir1/file_a')
|
||||
self.assertEqual(r.lastmod, '2012-07-25T17:13:46Z')
|
||||
self.assertEqual(r.md5, None)
|
||||
self.assertEqual(r.length, 20)
|
||||
self.assertEqual(r.path, 'tests/testdata/dir1/file_a')
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestResourceListBuilder)
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(
|
||||
TestResourceListBuilder)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -11,91 +11,95 @@ from resync.mapper import Mapper
|
||||
|
||||
# etree gives ParseError in 2.7, ExpatError in 2.6
|
||||
etree_error_class = None
|
||||
if (sys.version_info < (2,7)):
|
||||
if (sys.version_info < (2, 7)):
|
||||
from xml.parsers.expat import ExpatError
|
||||
etree_error_class = ExpatError
|
||||
else:
|
||||
from xml.etree.ElementTree import ParseError
|
||||
etree_error_class = ParseError
|
||||
|
||||
|
||||
class TestResourceListMultifile(unittest.TestCase):
|
||||
|
||||
def test_01_read_local_filenames(self):
|
||||
rl = ResourceList()
|
||||
rl.read('tests/testdata/sitemapindex2/sitemap.xml')
|
||||
self.assertEqual( len(rl.resources), 17, '17 resources from 3 sitemaps listed')
|
||||
self.assertEqual(len(rl.resources), 17,
|
||||
'17 resources from 3 sitemaps listed')
|
||||
sr = sorted(rl.uris())
|
||||
self.assertEqual( sr[0], 'http://localhost:8888/resources/1' )
|
||||
self.assertEqual( sr[1], 'http://localhost:8888/resources/10' )
|
||||
self.assertEqual( sr[2], 'http://localhost:8888/resources/100' )
|
||||
self.assertEqual( sr[3], 'http://localhost:8888/resources/1000' )
|
||||
self.assertEqual( sr[16], 'http://localhost:8888/resources/826' )
|
||||
self.assertEqual(sr[0], 'http://localhost:8888/resources/1')
|
||||
self.assertEqual(sr[1], 'http://localhost:8888/resources/10')
|
||||
self.assertEqual(sr[2], 'http://localhost:8888/resources/100')
|
||||
self.assertEqual(sr[3], 'http://localhost:8888/resources/1000')
|
||||
self.assertEqual(sr[16], 'http://localhost:8888/resources/826')
|
||||
|
||||
def test_02_read_with_mapper(self):
|
||||
rl = ResourceList()
|
||||
rl.mapper = Mapper(['http://localhost/=tests/testdata/sitemapindex2/'])
|
||||
rl.read('tests/testdata/sitemapindex2/sitemap_mapper.xml')
|
||||
self.assertEqual( len(rl.resources), 17, '17 resources from 3 sitemaps listed')
|
||||
self.assertEqual(len(rl.resources), 17,
|
||||
'17 resources from 3 sitemaps listed')
|
||||
sr = sorted(rl.uris())
|
||||
self.assertEqual( sr[0], 'http://localhost:8888/resources/1' )
|
||||
self.assertEqual( sr[1], 'http://localhost:8888/resources/10' )
|
||||
self.assertEqual( sr[2], 'http://localhost:8888/resources/100' )
|
||||
self.assertEqual( sr[3], 'http://localhost:8888/resources/1000' )
|
||||
self.assertEqual( sr[16], 'http://localhost:8888/resources/826' )
|
||||
self.assertEqual(sr[0], 'http://localhost:8888/resources/1')
|
||||
self.assertEqual(sr[1], 'http://localhost:8888/resources/10')
|
||||
self.assertEqual(sr[2], 'http://localhost:8888/resources/100')
|
||||
self.assertEqual(sr[3], 'http://localhost:8888/resources/1000')
|
||||
self.assertEqual(sr[16], 'http://localhost:8888/resources/826')
|
||||
|
||||
def test_11_write_multifile(self):
|
||||
tempdir = tempfile.mkdtemp(prefix='test_resource_list_multifile')
|
||||
rl = ResourceList()
|
||||
rl.mapper = Mapper(['http://localhost/=%s/' % (tempdir)])
|
||||
rl.add( Resource( uri='http://localhost/a' ) )
|
||||
rl.add( Resource( uri='http://localhost/b' ) )
|
||||
rl.add( Resource( uri='http://localhost/c' ) )
|
||||
rl.add( Resource( uri='http://localhost/d' ) )
|
||||
rl.add(Resource(uri='http://localhost/a'))
|
||||
rl.add(Resource(uri='http://localhost/b'))
|
||||
rl.add(Resource(uri='http://localhost/c'))
|
||||
rl.add(Resource(uri='http://localhost/d'))
|
||||
rl.max_sitemap_entries = 2
|
||||
# first try writing without mutlifile allowed
|
||||
rl.allow_multifile = False
|
||||
self.assertRaises( ListBaseIndexError, rl.write, basename=os.path.join(tempdir,'sitemap.xml') )
|
||||
self.assertRaises(ListBaseIndexError, rl.write,
|
||||
basename=os.path.join(tempdir, 'sitemap.xml'))
|
||||
# second actually do it
|
||||
rl.allow_multifile = True
|
||||
rl.write( basename = os.path.join(tempdir,'sitemap.xml') )
|
||||
rl.write(basename=os.path.join(tempdir, 'sitemap.xml'))
|
||||
# check the two component sitemaps
|
||||
rl1 = ResourceList()
|
||||
rl1.read( os.path.join(tempdir,'sitemap00000.xml') )
|
||||
self.assertEquals( len(rl1), 2 )
|
||||
self.assertEquals( rl1.capability, 'resourcelist' )
|
||||
self.assertFalse( rl1.sitemapindex )
|
||||
rl1.read(os.path.join(tempdir, 'sitemap00000.xml'))
|
||||
self.assertEquals(len(rl1), 2)
|
||||
self.assertEquals(rl1.capability, 'resourcelist')
|
||||
self.assertFalse(rl1.sitemapindex)
|
||||
i = iter(rl1)
|
||||
self.assertEquals( next(i).uri, 'http://localhost/a' )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/b' )
|
||||
self.assertEquals(next(i).uri, 'http://localhost/a')
|
||||
self.assertEquals(next(i).uri, 'http://localhost/b')
|
||||
rl2 = ResourceList()
|
||||
rl2.read( os.path.join(tempdir,'sitemap00001.xml') )
|
||||
self.assertEquals( len(rl2), 2 )
|
||||
rl2.read(os.path.join(tempdir, 'sitemap00001.xml'))
|
||||
self.assertEquals(len(rl2), 2)
|
||||
i = iter(rl2)
|
||||
self.assertEquals( next(i).uri, 'http://localhost/c' )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/d' )
|
||||
self.assertEquals(next(i).uri, 'http://localhost/c')
|
||||
self.assertEquals(next(i).uri, 'http://localhost/d')
|
||||
# check the sitemapindex (read just as index)
|
||||
rli = ResourceList()
|
||||
rli.read( os.path.join(tempdir,'sitemap.xml'), index_only=True )
|
||||
self.assertEquals( len(rli), 2 )
|
||||
rli.read(os.path.join(tempdir, 'sitemap.xml'), index_only=True)
|
||||
self.assertEquals(len(rli), 2)
|
||||
i = iter(rli)
|
||||
self.assertEquals( rli.capability, 'resourcelist' )
|
||||
self.assertTrue( rli.sitemapindex )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/sitemap00000.xml' )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/sitemap00001.xml' )
|
||||
self.assertEquals(rli.capability, 'resourcelist')
|
||||
self.assertTrue(rli.sitemapindex)
|
||||
self.assertEquals(next(i).uri, 'http://localhost/sitemap00000.xml')
|
||||
self.assertEquals(next(i).uri, 'http://localhost/sitemap00001.xml')
|
||||
# check the sitemapindex and components
|
||||
rli = ResourceList( mapper=rl.mapper )
|
||||
rli.read( os.path.join(tempdir,'sitemap.xml') )
|
||||
self.assertEquals( len(rli), 4 )
|
||||
self.assertEquals( rli.capability, 'resourcelist' )
|
||||
self.assertFalse( rli.sitemapindex )
|
||||
rli = ResourceList(mapper=rl.mapper)
|
||||
rli.read(os.path.join(tempdir, 'sitemap.xml'))
|
||||
self.assertEquals(len(rli), 4)
|
||||
self.assertEquals(rli.capability, 'resourcelist')
|
||||
self.assertFalse(rli.sitemapindex)
|
||||
i = iter(rli)
|
||||
self.assertEquals( next(i).uri, 'http://localhost/a' )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/b' )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/c' )
|
||||
self.assertEquals( next(i).uri, 'http://localhost/d' )
|
||||
self.assertEquals(next(i).uri, 'http://localhost/a')
|
||||
self.assertEquals(next(i).uri, 'http://localhost/b')
|
||||
self.assertEquals(next(i).uri, 'http://localhost/c')
|
||||
self.assertEquals(next(i).uri, 'http://localhost/d')
|
||||
# cleanup tempdir
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestResourceListMultifile)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -1,37 +1,38 @@
|
||||
import unittest
|
||||
import re
|
||||
from resync.resource import Resource
|
||||
from resync.resource_set import ResourceSet,ResourceSetDupeError
|
||||
from resync.resource_set import ResourceSet, ResourceSetDupeError
|
||||
|
||||
|
||||
class TestResourceSet(unittest.TestCase):
|
||||
|
||||
def test01_add(self):
|
||||
rs = ResourceSet()
|
||||
self.assertEqual( len(rs), 0 )
|
||||
rs.add( Resource('a') )
|
||||
self.assertEqual( len(rs), 1 )
|
||||
rs.add( Resource('b') )
|
||||
self.assertEqual( len(rs), 2 )
|
||||
rs.add( Resource('c') )
|
||||
self.assertEqual( len(rs), 3 )
|
||||
self.assertEqual(len(rs), 0)
|
||||
rs.add(Resource('a'))
|
||||
self.assertEqual(len(rs), 1)
|
||||
rs.add(Resource('b'))
|
||||
self.assertEqual(len(rs), 2)
|
||||
rs.add(Resource('c'))
|
||||
self.assertEqual(len(rs), 3)
|
||||
|
||||
def test02_order(self):
|
||||
rs = ResourceSet()
|
||||
rs.add( Resource('a2') )
|
||||
rs.add( Resource('a3') )
|
||||
rs.add( Resource('a1') )
|
||||
rs.add(Resource('a2'))
|
||||
rs.add(Resource('a3'))
|
||||
rs.add(Resource('a1'))
|
||||
i = iter(rs)
|
||||
self.assertEqual( next(i).uri, 'a1' )
|
||||
self.assertEqual( next(i).uri, 'a2' )
|
||||
self.assertEqual( next(i).uri, 'a3' )
|
||||
self.assertRaises( StopIteration, next, i )
|
||||
self.assertEqual(next(i).uri, 'a1')
|
||||
self.assertEqual(next(i).uri, 'a2')
|
||||
self.assertEqual(next(i).uri, 'a3')
|
||||
self.assertRaises(StopIteration, next, i)
|
||||
|
||||
def test03_dupe(self):
|
||||
rs = ResourceSet()
|
||||
self.assertEqual( len(rs), 0 )
|
||||
rs.add( Resource('a') )
|
||||
self.assertEqual( len(rs), 1 )
|
||||
self.assertRaises( ResourceSetDupeError, rs.add, Resource('a') )
|
||||
self.assertEqual(len(rs), 0)
|
||||
rs.add(Resource('a'))
|
||||
self.assertEqual(len(rs), 1)
|
||||
self.assertRaises(ResourceSetDupeError, rs.add, Resource('a'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import sys
|
||||
import unittest
|
||||
try: #python2
|
||||
try: # python2
|
||||
# Must try this first as io also exists in python2
|
||||
# but in the wrong one!
|
||||
import StringIO as io
|
||||
except ImportError: #python3
|
||||
except ImportError: # python3
|
||||
import io
|
||||
|
||||
from resync.resource import Resource
|
||||
@ -13,159 +13,173 @@ from resync.sitemap import Sitemap, SitemapIndexError, SitemapParseError
|
||||
|
||||
# etree gives ParseError in 2.7,3.x; ExpatError in 2.6
|
||||
etree_error_class = None
|
||||
if (sys.version_info < (2,7)):
|
||||
if (sys.version_info < (2, 7)):
|
||||
from xml.parsers.expat import ExpatError
|
||||
etree_error_class = ExpatError
|
||||
else:
|
||||
#In python3 this seems only to work with the full class name??
|
||||
#from xml.etree.ElementTree import ParseError
|
||||
# In python3 this seems only to work with the full class name??
|
||||
# from xml.etree.ElementTree import ParseError
|
||||
import xml.etree.ElementTree
|
||||
etree_error_class = xml.etree.ElementTree.ParseError
|
||||
|
||||
|
||||
class TestSitemap(unittest.TestCase):
|
||||
|
||||
def test_01_resource_str(self):
|
||||
r1 = Resource('a3')
|
||||
r1.lastmod='2012-01-11T01:02:03Z'
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>a3</loc><lastmod>2012-01-11T01:02:03Z</lastmod></url>" )
|
||||
r1.lastmod = '2012-01-11T01:02:03Z'
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>a3</loc><lastmod>2012-01-11T01:02:03Z</lastmod></url>")
|
||||
|
||||
def test_02_resource_str(self):
|
||||
r1 = Resource('3b',1234.1,9999,'ab54de')
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>3b</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:ab54de\" length=\"9999\" /></url>" )
|
||||
r1 = Resource('3b', 1234.1, 9999, 'ab54de')
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>3b</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:ab54de\" length=\"9999\" /></url>")
|
||||
|
||||
def test_03_resource_str_hashes(self):
|
||||
r1 = Resource('03hashes',1234.1)
|
||||
r1 = Resource('03hashes', 1234.1)
|
||||
r1.md5 = 'aaa'
|
||||
r1.sha1 = 'bbb'
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>03hashes</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:aaa sha-1:bbb\" /></url>" )
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>03hashes</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:aaa sha-1:bbb\" /></url>")
|
||||
r1.sha256 = 'ccc'
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>03hashes</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:aaa sha-1:bbb sha-256:ccc\" /></url>" )
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>03hashes</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:aaa sha-1:bbb sha-256:ccc\" /></url>")
|
||||
r1.sha1 = None
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>03hashes</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:aaa sha-256:ccc\" /></url>" )
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>03hashes</loc><lastmod>1970-01-01T00:20:34.100000Z</lastmod><rs:md hash=\"md5:aaa sha-256:ccc\" /></url>")
|
||||
|
||||
def test_04_resource_str(self):
|
||||
r1 = Resource(uri='4a',lastmod="2013-01-02",length=9999,md5='ab54de')
|
||||
r1.ln = [{ 'rel':'duplicate',
|
||||
'pri':'1',
|
||||
'href':'http://mirror1.example.com/res1',
|
||||
'modified':'2013-01-02T18:00:00Z' }]
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>4a</loc><lastmod>2013-01-02T00:00:00Z</lastmod><rs:md hash=\"md5:ab54de\" length=\"9999\" /><rs:ln href=\"http://mirror1.example.com/res1\" modified=\"2013-01-02T18:00:00Z\" pri=\"1\" rel=\"duplicate\" /></url>" )
|
||||
r1 = Resource(uri='4a', lastmod="2013-01-02",
|
||||
length=9999, md5='ab54de')
|
||||
r1.ln = [{'rel': 'duplicate',
|
||||
'pri': '1',
|
||||
'href': 'http://mirror1.example.com/res1',
|
||||
'modified': '2013-01-02T18:00:00Z'}]
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>4a</loc><lastmod>2013-01-02T00:00:00Z</lastmod><rs:md hash=\"md5:ab54de\" length=\"9999\" /><rs:ln href=\"http://mirror1.example.com/res1\" modified=\"2013-01-02T18:00:00Z\" pri=\"1\" rel=\"duplicate\" /></url>")
|
||||
# add another two rs:ln's
|
||||
r1.ln.append( { 'rel':'num2' } )
|
||||
r1.ln.append( { 'rel':'num3' } )
|
||||
self.assertEqual( Sitemap().resource_as_xml(r1), "<url><loc>4a</loc><lastmod>2013-01-02T00:00:00Z</lastmod><rs:md hash=\"md5:ab54de\" length=\"9999\" /><rs:ln href=\"http://mirror1.example.com/res1\" modified=\"2013-01-02T18:00:00Z\" pri=\"1\" rel=\"duplicate\" /><rs:ln rel=\"num2\" /><rs:ln rel=\"num3\" /></url>" )
|
||||
r1.ln.append({'rel': 'num2'})
|
||||
r1.ln.append({'rel': 'num3'})
|
||||
self.assertEqual(Sitemap().resource_as_xml(
|
||||
r1), "<url><loc>4a</loc><lastmod>2013-01-02T00:00:00Z</lastmod><rs:md hash=\"md5:ab54de\" length=\"9999\" /><rs:ln href=\"http://mirror1.example.com/res1\" modified=\"2013-01-02T18:00:00Z\" pri=\"1\" rel=\"duplicate\" /><rs:ln rel=\"num2\" /><rs:ln rel=\"num3\" /></url>")
|
||||
|
||||
def test_08_print(self):
|
||||
r1 = Resource(uri='a',lastmod='2001-01-01',length=1234)
|
||||
r2 = Resource(uri='b',lastmod='2002-02-02',length=56789)
|
||||
r3 = Resource(uri='c',lastmod='2003-03-03',length=0)
|
||||
m = ResourceList(md={'capability':'resourcelist','modified':None})
|
||||
r1 = Resource(uri='a', lastmod='2001-01-01', length=1234)
|
||||
r2 = Resource(uri='b', lastmod='2002-02-02', length=56789)
|
||||
r3 = Resource(uri='c', lastmod='2003-03-03', length=0)
|
||||
m = ResourceList(md={'capability': 'resourcelist', 'modified': None})
|
||||
m.add(r1)
|
||||
m.add(r2)
|
||||
m.add(r3)
|
||||
#print m
|
||||
self.assertEqual( Sitemap().resources_as_xml(m), "<?xml version='1.0' encoding='UTF-8'?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:rs=\"http://www.openarchives.org/rs/terms/\"><rs:md capability=\"resourcelist\" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length=\"1234\" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length=\"56789\" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length=\"0\" /></url></urlset>")
|
||||
# print m
|
||||
self.assertEqual(Sitemap().resources_as_xml(m), "<?xml version='1.0' encoding='UTF-8'?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:rs=\"http://www.openarchives.org/rs/terms/\"><rs:md capability=\"resourcelist\" /><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length=\"1234\" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length=\"56789\" /></url><url><loc>c</loc><lastmod>2003-03-03T00:00:00Z</lastmod><rs:md length=\"0\" /></url></urlset>")
|
||||
|
||||
def test_09_print_from_iter(self):
|
||||
r1 = Resource(uri='a',lastmod='2001-01-01',length=1234)
|
||||
r2 = Resource(uri='b',lastmod='2002-02-02',length=56789)
|
||||
def test_09_print_from_iter(self):
|
||||
r1 = Resource(uri='a', lastmod='2001-01-01', length=1234)
|
||||
r2 = Resource(uri='b', lastmod='2002-02-02', length=56789)
|
||||
m = ResourceList()
|
||||
m.add(r1)
|
||||
m.add(r2)
|
||||
i = iter(m)
|
||||
self.assertEqual( Sitemap().resources_as_xml(i), "<?xml version='1.0' encoding='UTF-8'?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:rs=\"http://www.openarchives.org/rs/terms/\"><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length=\"1234\" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length=\"56789\" /></url></urlset>")
|
||||
self.assertEqual(Sitemap().resources_as_xml(i), "<?xml version='1.0' encoding='UTF-8'?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:rs=\"http://www.openarchives.org/rs/terms/\"><url><loc>a</loc><lastmod>2001-01-01T00:00:00Z</lastmod><rs:md length=\"1234\" /></url><url><loc>b</loc><lastmod>2002-02-02T00:00:00Z</lastmod><rs:md length=\"56789\" /></url></urlset>")
|
||||
|
||||
def test_10_sitemap(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<url><loc>http://e.com/a</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md hash="md5:Q2hlY2sgSW50ZWdyaXR5IQ==" length=\"12\" /></url>\
|
||||
</urlset>'
|
||||
s=Sitemap()
|
||||
i=s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertFalse( s.parsed_index, 'was a sitemap')
|
||||
self.assertEqual( s.resources_created, 1, 'got 1 resources')
|
||||
s = Sitemap()
|
||||
i = s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertFalse(s.parsed_index, 'was a sitemap')
|
||||
self.assertEqual(s.resources_created, 1, 'got 1 resources')
|
||||
for r in i.resources:
|
||||
self.assertTrue( r is not None, 'got the uri expected')
|
||||
self.assertEqual( r.uri, 'http://e.com/a' )
|
||||
self.assertEqual( r.lastmod, '2012-03-14T18:37:36Z' )
|
||||
self.assertEqual( r.length, 12 )
|
||||
self.assertEqual( r.md5, 'Q2hlY2sgSW50ZWdyaXR5IQ==' )
|
||||
self.assertTrue(r is not None, 'got the uri expected')
|
||||
self.assertEqual(r.uri, 'http://e.com/a')
|
||||
self.assertEqual(r.lastmod, '2012-03-14T18:37:36Z')
|
||||
self.assertEqual(r.length, 12)
|
||||
self.assertEqual(r.md5, 'Q2hlY2sgSW50ZWdyaXR5IQ==')
|
||||
|
||||
def test_11_parse_2(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<url><loc>/tmp/rs_test/src/file_a</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length=\"12\" /></url>\
|
||||
<url><loc>/tmp/rs_test/src/file_b</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length=\"32\" /></url>\
|
||||
</urlset>'
|
||||
s=Sitemap()
|
||||
i=s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertFalse( s.parsed_index, 'was a sitemap')
|
||||
self.assertEqual( s.resources_created, 2, 'got 2 resources')
|
||||
s = Sitemap()
|
||||
i = s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertFalse(s.parsed_index, 'was a sitemap')
|
||||
self.assertEqual(s.resources_created, 2, 'got 2 resources')
|
||||
|
||||
def test_12_parse_multi_loc(self):
|
||||
xml_start='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml_start = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<url>'
|
||||
xml_end='<lastmod>2012-03-14T18:37:36Z</lastmod></url></urlset>'
|
||||
s=Sitemap()
|
||||
two_loc='<loc>/tmp/rs_test/src/file_a</loc><loc>/tmp/rs_test/src/file_b</loc>'
|
||||
self.assertRaises( SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start+two_loc+xml_end))
|
||||
mt_loc='<loc></loc>'
|
||||
self.assertRaises( SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start+mt_loc+xml_end))
|
||||
mt_loc_att='<loc att="value"/>'
|
||||
self.assertRaises( SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start+mt_loc_att+xml_end))
|
||||
xml_end = '<lastmod>2012-03-14T18:37:36Z</lastmod></url></urlset>'
|
||||
s = Sitemap()
|
||||
two_loc = '<loc>/tmp/rs_test/src/file_a</loc><loc>/tmp/rs_test/src/file_b</loc>'
|
||||
self.assertRaises(SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start + two_loc + xml_end))
|
||||
mt_loc = '<loc></loc>'
|
||||
self.assertRaises(SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start + mt_loc + xml_end))
|
||||
mt_loc_att = '<loc att="value"/>'
|
||||
self.assertRaises(SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start + mt_loc_att + xml_end))
|
||||
|
||||
def test_13_parse_multi_lastmod(self):
|
||||
xml_start='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml_start = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<url><loc>uri:a</loc>'
|
||||
xml_end='</url></urlset>'
|
||||
s=Sitemap()
|
||||
two_lastmod='<lastmod>2013-01-01</lastmod><lastmod>2013-01-02</lastmod>'
|
||||
self.assertRaises( SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start+two_lastmod+xml_end))
|
||||
xml_end = '</url></urlset>'
|
||||
s = Sitemap()
|
||||
two_lastmod = '<lastmod>2013-01-01</lastmod><lastmod>2013-01-02</lastmod>'
|
||||
self.assertRaises(SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start + two_lastmod + xml_end))
|
||||
# While it not ideal to omit, <lastmod> is not required and
|
||||
# thus either empty lastmod or lastmod with just an attribute
|
||||
# and no content are not ambiguous and thus should be accepted
|
||||
# with resulting None for resource.lastmod
|
||||
mt_lastmod='<lastmod></lastmod>'
|
||||
i=s.parse_xml(fh=io.StringIO(xml_start+mt_lastmod+xml_end))
|
||||
self.assertEqual( s.resources_created, 1 )
|
||||
self.assertEqual( i.resources[0].lastmod, None )
|
||||
mt_lastmod_att='<lastmod att="value"/>'
|
||||
i=s.parse_xml(fh=io.StringIO(xml_start+mt_lastmod_att+xml_end))
|
||||
self.assertEqual( s.resources_created, 1 )
|
||||
self.assertEqual( i.resources[0].lastmod, None )
|
||||
mt_lastmod = '<lastmod></lastmod>'
|
||||
i = s.parse_xml(fh=io.StringIO(xml_start + mt_lastmod + xml_end))
|
||||
self.assertEqual(s.resources_created, 1)
|
||||
self.assertEqual(i.resources[0].lastmod, None)
|
||||
mt_lastmod_att = '<lastmod att="value"/>'
|
||||
i = s.parse_xml(fh=io.StringIO(xml_start + mt_lastmod_att + xml_end))
|
||||
self.assertEqual(s.resources_created, 1)
|
||||
self.assertEqual(i.resources[0].lastmod, None)
|
||||
|
||||
def test_14_parse_multi(self):
|
||||
xml_start='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml_start = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">'
|
||||
xml_end='<url><loc>uri:a</loc></url></urlset>'
|
||||
s=Sitemap()
|
||||
two_md='<rs:md capability="resourcelist"/><rs:md capability="resourcelist"/>'
|
||||
self.assertRaises( SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start+two_md+xml_end))
|
||||
xml_end = '<url><loc>uri:a</loc></url></urlset>'
|
||||
s = Sitemap()
|
||||
two_md = '<rs:md capability="resourcelist"/><rs:md capability="resourcelist"/>'
|
||||
self.assertRaises(SitemapParseError, s.parse_xml,
|
||||
io.StringIO(xml_start + two_md + xml_end))
|
||||
|
||||
def test_15_parse_illformed(self):
|
||||
s=Sitemap()
|
||||
s = Sitemap()
|
||||
# ExpatError in python2.6, ParserError in 2.7,3.x
|
||||
self.assertRaises( etree_error_class, s.parse_xml, io.StringIO('not xml') )
|
||||
self.assertRaises( etree_error_class, s.parse_xml, io.StringIO('<urlset><url>something</urlset>') )
|
||||
self.assertRaises(etree_error_class, s.parse_xml,
|
||||
io.StringIO('not xml'))
|
||||
self.assertRaises(etree_error_class, s.parse_xml,
|
||||
io.StringIO('<urlset><url>something</urlset>'))
|
||||
|
||||
def test_16_parse_valid_xml_but_other(self):
|
||||
s=Sitemap()
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, io.StringIO('<urlset xmlns="http://example.org/other_namespace"> </urlset>') )
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, io.StringIO('<other xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </other>') )
|
||||
s = Sitemap()
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, io.StringIO(
|
||||
'<urlset xmlns="http://example.org/other_namespace"> </urlset>'))
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, io.StringIO(
|
||||
'<other xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </other>'))
|
||||
|
||||
def test_17_parse_sitemapindex_as_sitemap(self):
|
||||
s=Sitemap()
|
||||
self.assertRaises( SitemapIndexError, s.parse_xml, io.StringIO('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </sitemapindex>'), sitemapindex=False )
|
||||
s = Sitemap()
|
||||
self.assertRaises(SitemapIndexError, s.parse_xml, io.StringIO(
|
||||
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </sitemapindex>'), sitemapindex=False)
|
||||
|
||||
def test_18_parse_with_rs_ln_on_resource(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability=\"resourcelist\"/>\
|
||||
<url>\
|
||||
@ -180,103 +194,108 @@ class TestSitemap(unittest.TestCase):
|
||||
<rs:md length=\"32\" />\
|
||||
</url>\
|
||||
</urlset>'
|
||||
s=Sitemap()
|
||||
rc=s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertFalse( s.parsed_index, 'was a sitemap')
|
||||
self.assertEqual( s.resources_created, 2, 'got 2 resources')
|
||||
s = Sitemap()
|
||||
rc = s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertFalse(s.parsed_index, 'was a sitemap')
|
||||
self.assertEqual(s.resources_created, 2, 'got 2 resources')
|
||||
i = iter(rc)
|
||||
r1 = next(i)
|
||||
r2 = next(i)
|
||||
self.assertEqual( r1.uri, 'http://example.com/file_a' )
|
||||
self.assertEqual( r1.ln[0]['rel'], 'duplicate' )
|
||||
self.assertEqual( r1.ln[0]['href'], 'http://mirror1.example.com/res1' )
|
||||
self.assertEqual( r1.ln[0]['modified'], '2013-01-02' )
|
||||
self.assertEqual( r1.ln[0]['pri'], 1 )
|
||||
self.assertEqual( r2.uri, 'http://example.com/file_b' )
|
||||
self.assertEqual(r1.uri, 'http://example.com/file_a')
|
||||
self.assertEqual(r1.ln[0]['rel'], 'duplicate')
|
||||
self.assertEqual(r1.ln[0]['href'], 'http://mirror1.example.com/res1')
|
||||
self.assertEqual(r1.ln[0]['modified'], '2013-01-02')
|
||||
self.assertEqual(r1.ln[0]['pri'], 1)
|
||||
self.assertEqual(r2.uri, 'http://example.com/file_b')
|
||||
|
||||
def test_19_parse_with_bad_rs_ln(self):
|
||||
xmlstart='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xmlstart = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<rs:md capability="resourcelist"/>\
|
||||
<url><loc>http://example.com/file_a</loc>'
|
||||
xmlend='</url></urlset>'
|
||||
s=Sitemap()
|
||||
xmlend = '</url></urlset>'
|
||||
s = Sitemap()
|
||||
#
|
||||
# missing href
|
||||
xml=xmlstart+'<rs:ln rel="duplicate"/>'+xmlend
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml = xmlstart + '<rs:ln rel="duplicate"/>' + xmlend
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
# missing rel
|
||||
xml=xmlstart+'<rs:ln href="http://example.com/"/>'+xmlend
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml = xmlstart + '<rs:ln href="http://example.com/"/>' + xmlend
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
# bad length
|
||||
xml=xmlstart+'<rs:ln rel="duplicate" href="http://example.com/" length="a"/>'+xmlend
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml = xmlstart + '<rs:ln rel="duplicate" href="http://example.com/" length="a"/>' + xmlend
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
# bad pri
|
||||
xml=xmlstart+'<rs:ln rel="duplicate" href="http://example.com/" pri="fff"/>'+xmlend
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml=xmlstart+'<rs:ln rel="duplicate" href="http://example.com/" pri="0"/>'+xmlend
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml=xmlstart+'<rs:ln rel="duplicate" href="http://example.com/" pri="1000000"/>'+xmlend
|
||||
self.assertRaises( SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml = xmlstart + '<rs:ln rel="duplicate" href="http://example.com/" pri="fff"/>' + xmlend
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml = xmlstart + '<rs:ln rel="duplicate" href="http://example.com/" pri="0"/>' + xmlend
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
xml = xmlstart + '<rs:ln rel="duplicate" href="http://example.com/" pri="1000000"/>' + xmlend
|
||||
self.assertRaises(SitemapParseError, s.parse_xml, fh=io.StringIO(xml))
|
||||
# and finally OK with errors fixes
|
||||
xml=xmlstart+'<rs:ln rel="duplicate" href="http://example.com/" length="12345" pri="1" other="whatever"/>'+xmlend
|
||||
xml = xmlstart + '<rs:ln rel="duplicate" href="http://example.com/" length="12345" pri="1" other="whatever"/>' + xmlend
|
||||
rc = s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertEqual( len(rc.resources), 1, 'good at last, extra attribute ignored' )
|
||||
self.assertEqual(len(rc.resources), 1,
|
||||
'good at last, extra attribute ignored')
|
||||
|
||||
def test_20_parse_sitemapindex_empty(self):
|
||||
s=Sitemap()
|
||||
si = s.parse_xml( fh=io.StringIO('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </sitemapindex>'), sitemapindex=True )
|
||||
self.assertTrue( s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual( len(si.resources), 0, '0 sitemaps')
|
||||
s = Sitemap()
|
||||
si = s.parse_xml(fh=io.StringIO(
|
||||
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> </sitemapindex>'), sitemapindex=True)
|
||||
self.assertTrue(s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual(len(si.resources), 0, '0 sitemaps')
|
||||
|
||||
def test_21_parse_sitemapindex(self):
|
||||
s=Sitemap()
|
||||
si = s.parse_xml( fh=io.StringIO('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>aaa</loc></sitemap><sitemap><loc>bbb</loc></sitemap></sitemapindex>'), sitemapindex=True )
|
||||
self.assertEqual( len(si.resources), 2, '2 sitemaps')
|
||||
s = Sitemap()
|
||||
si = s.parse_xml(fh=io.StringIO(
|
||||
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>aaa</loc></sitemap><sitemap><loc>bbb</loc></sitemap></sitemapindex>'), sitemapindex=True)
|
||||
self.assertEqual(len(si.resources), 2, '2 sitemaps')
|
||||
sms = sorted(si.uris())
|
||||
self.assertEqual( sms, ['aaa','bbb'] )
|
||||
self.assertEqual(sms, ['aaa', 'bbb'])
|
||||
# add a couple more
|
||||
s.parse_xml( fh=io.StringIO('<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>cc</loc></sitemap><sitemap><loc>dd</loc></sitemap></sitemapindex>'), resources=si )
|
||||
self.assertTrue( s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual( len(si.resources), 4, '4 sitemaps total')
|
||||
s.parse_xml(fh=io.StringIO(
|
||||
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>cc</loc></sitemap><sitemap><loc>dd</loc></sitemap></sitemapindex>'), resources=si)
|
||||
self.assertTrue(s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual(len(si.resources), 4, '4 sitemaps total')
|
||||
sms = sorted(si.uris())
|
||||
self.assertEqual( sms, ['aaa','bbb', 'cc', 'dd'] )
|
||||
self.assertEqual(sms, ['aaa', 'bbb', 'cc', 'dd'])
|
||||
|
||||
def test_22_parse_sitemapindex_file(self):
|
||||
s=Sitemap()
|
||||
fh=open('tests/testdata/sitemapindex1/sitemap.xml','r')
|
||||
si = s.parse_xml( fh=fh, sitemapindex=True )
|
||||
self.assertTrue( s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual( len(si.resources), 3, '3 sitemaps')
|
||||
s = Sitemap()
|
||||
fh = open('tests/testdata/sitemapindex1/sitemap.xml', 'r')
|
||||
si = s.parse_xml(fh=fh, sitemapindex=True)
|
||||
self.assertTrue(s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual(len(si.resources), 3, '3 sitemaps')
|
||||
sms = sorted(si.uris())
|
||||
self.assertEqual( sms, ['http://localhost:8888/sitemap00000.xml','http://localhost:8888/sitemap00001.xml','http://localhost:8888/sitemap00002.xml'] )
|
||||
#self.assertEqual( si.resources['http://localhost:8888/sitemap00000.xml'].lastmod, '2012-06-13T18:09:13Z' )
|
||||
self.assertEqual(sms, ['http://localhost:8888/sitemap00000.xml',
|
||||
'http://localhost:8888/sitemap00001.xml', 'http://localhost:8888/sitemap00002.xml'])
|
||||
# self.assertEqual( si.resources['http://localhost:8888/sitemap00000.xml'].lastmod, '2012-06-13T18:09:13Z' )
|
||||
|
||||
def test_21_parse_multi_sitemapindex(self):
|
||||
s = Sitemap()
|
||||
fh=open('tests/testdata/sitemapindex2/sitemap.xml','r')
|
||||
si = s.parse_xml( fh=fh, sitemapindex=True )
|
||||
self.assertTrue( s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual( len(si.resources), 3, '3 sitemaps listed')
|
||||
fh = open('tests/testdata/sitemapindex2/sitemap.xml', 'r')
|
||||
si = s.parse_xml(fh=fh, sitemapindex=True)
|
||||
self.assertTrue(s.parsed_index, 'was a sitemapindex')
|
||||
self.assertEqual(len(si.resources), 3, '3 sitemaps listed')
|
||||
|
||||
def test_30_parse_change_list(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n\
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/">\
|
||||
<url><loc>/tmp/rs_test/src/file_a</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md change="updated" length="12" /></url>\
|
||||
<url><loc>/tmp/rs_test/src/file_b</loc><lastmod>2012-03-14T18:37:36Z</lastmod><rs:md length="32" /></url>\
|
||||
</urlset>'
|
||||
s=Sitemap()
|
||||
s.resource_class=Resource
|
||||
c=s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertEqual( s.resources_created, 2, 'got 2 resources')
|
||||
s = Sitemap()
|
||||
s.resource_class = Resource
|
||||
c = s.parse_xml(fh=io.StringIO(xml))
|
||||
self.assertEqual(s.resources_created, 2, 'got 2 resources')
|
||||
i = iter(c)
|
||||
r1 = next(i)
|
||||
self.assertEqual( r1.uri, '/tmp/rs_test/src/file_a' )
|
||||
self.assertEqual( r1.change, 'updated' )
|
||||
self.assertEqual(r1.uri, '/tmp/rs_test/src/file_a')
|
||||
self.assertEqual(r1.change, 'updated')
|
||||
r2 = next(i)
|
||||
self.assertEqual( r2.uri, '/tmp/rs_test/src/file_b' )
|
||||
self.assertEqual( r2.change, None )
|
||||
self.assertEqual(r2.uri, '/tmp/rs_test/src/file_b')
|
||||
self.assertEqual(r2.change, None)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestSitemap)
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
@ -3,46 +3,48 @@ import re
|
||||
from resync.resource import Resource
|
||||
from resync.source_description import SourceDescription
|
||||
|
||||
|
||||
class TestSourceDescription(unittest.TestCase):
|
||||
|
||||
def test01_empty(self):
|
||||
rsd = SourceDescription()
|
||||
rsd.describedby = "http://example.org/about"
|
||||
self.assertEqual( len(rsd), 0 )
|
||||
self.assertEqual(len(rsd), 0)
|
||||
rsd.md_at = None
|
||||
self.assertEqual( rsd.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /></urlset>' )
|
||||
self.assertEqual(rsd.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /></urlset>')
|
||||
|
||||
def test02_one_caplist(self):
|
||||
rsd = SourceDescription()
|
||||
rsd.describedby = "http://example.org/about"
|
||||
self.assertEqual( len(rsd), 0 )
|
||||
self.assertEqual(len(rsd), 0)
|
||||
rsd.md_at = None
|
||||
rsd.add_capability_list("http://example.org/ds1/cl.xml")
|
||||
self.assertEqual( len(rsd), 1 )
|
||||
self.assertEqual( rsd.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /><url><loc>http://example.org/ds1/cl.xml</loc><rs:md capability="capabilitylist" /></url></urlset>' )
|
||||
self.assertEqual(len(rsd), 1)
|
||||
self.assertEqual(rsd.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /><url><loc>http://example.org/ds1/cl.xml</loc><rs:md capability="capabilitylist" /></url></urlset>')
|
||||
|
||||
def test03_a_bunch(self):
|
||||
rsd = SourceDescription()
|
||||
rsd.describedby = "http://example.org/about"
|
||||
self.assertEqual( len(rsd), 0 )
|
||||
self.assertEqual(len(rsd), 0)
|
||||
rsd.add_capability_list("http://example.org/ds1/cl.xml")
|
||||
rsd.add_capability_list("http://example.org/ds2/cl.xml")
|
||||
rsd.add_capability_list("http://example.org/ds3/cl.xml")
|
||||
self.assertEqual( len(rsd), 3 )
|
||||
self.assertEqual( rsd.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /><url><loc>http://example.org/ds1/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds2/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds3/cl.xml</loc><rs:md capability="capabilitylist" /></url></urlset>' )
|
||||
self.assertEqual(len(rsd), 3)
|
||||
self.assertEqual(rsd.as_xml(), '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /><url><loc>http://example.org/ds1/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds2/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds3/cl.xml</loc><rs:md capability="capabilitylist" /></url></urlset>')
|
||||
|
||||
def test04_parse(self):
|
||||
xml='<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /><url><loc>http://example.org/ds1/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds2/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds3/cl.xml</loc><rs:md capability="capabilitylist" /></url></urlset>'
|
||||
sd=SourceDescription()
|
||||
xml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:rs="http://www.openarchives.org/rs/terms/"><rs:ln href="http://example.org/about" rel="describedby" /><rs:md capability="description" /><url><loc>http://example.org/ds1/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds2/cl.xml</loc><rs:md capability="capabilitylist" /></url><url><loc>http://example.org/ds3/cl.xml</loc><rs:md capability="capabilitylist" /></url></urlset>'
|
||||
sd = SourceDescription()
|
||||
sd.parse(str_data=xml)
|
||||
self.assertEqual( sd.link_href('describedby'), 'http://example.org/about',
|
||||
'describedby link' )
|
||||
self.assertEqual( sd.capability, 'description' )
|
||||
self.assertEqual( len(sd.resources), 3, 'got 3 capacility lists' )
|
||||
[r1,r2,r3]=sd.resources
|
||||
self.assertEqual( r1.uri, 'http://example.org/ds1/cl.xml' )
|
||||
self.assertEqual( r1.capability, 'capabilitylist' )
|
||||
self.assertEqual(sd.link_href('describedby'), 'http://example.org/about',
|
||||
'describedby link')
|
||||
self.assertEqual(sd.capability, 'description')
|
||||
self.assertEqual(len(sd.resources), 3, 'got 3 capacility lists')
|
||||
[r1, r2, r3] = sd.resources
|
||||
self.assertEqual(r1.uri, 'http://example.org/ds1/cl.xml')
|
||||
self.assertEqual(r1.capability, 'capabilitylist')
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestSourceDescription)
|
||||
suite = unittest.defaultTestLoader.loadTestsFromTestCase(
|
||||
TestSourceDescription)
|
||||
unittest.TextTestRunner().run(suite)
|
||||
|
||||
@ -3,6 +3,7 @@ import unittest
|
||||
|
||||
from resync.url_authority import UrlAuthority
|
||||
|
||||
|
||||
class TestUrlAuthority(unittest.TestCase):
|
||||
|
||||
def test01_strict_authority(self):
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
import unittest
|
||||
import resync.utils
|
||||
|
||||
|
||||
class TestUtill(unittest.TestCase):
|
||||
|
||||
def test1_string(self):
|
||||
self.assertEqual( resync.utils.compute_md5_for_string('A file\n'),
|
||||
'j912liHgA/48DCHpkptJHg==')
|
||||
self.assertEqual(resync.utils.compute_md5_for_string('A file\n'),
|
||||
'j912liHgA/48DCHpkptJHg==')
|
||||
|
||||
def test2_file(self):
|
||||
# Should be same as the string above
|
||||
self.assertEqual( resync.utils.compute_md5_for_file('tests/testdata/a'),
|
||||
'j912liHgA/48DCHpkptJHg==')
|
||||
self.assertEqual(resync.utils.compute_md5_for_file('tests/testdata/a'),
|
||||
'j912liHgA/48DCHpkptJHg==')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@ -12,20 +12,27 @@ class TestW3cDatetime(unittest.TestCase):
|
||||
|
||||
def test01_datetime_to_str(self):
|
||||
"""Writing."""
|
||||
self.assertEqual(datetime_to_str(0), "1970-01-01T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(0),
|
||||
"1970-01-01T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(0.000001),
|
||||
"1970-01-01T00:00:00.000001Z")
|
||||
self.assertEqual(datetime_to_str(0.1), "1970-01-01T00:00:00.100000Z")
|
||||
self.assertEqual(datetime_to_str(1), "1970-01-01T00:00:01Z")
|
||||
self.assertEqual(datetime_to_str(60), "1970-01-01T00:01:00Z")
|
||||
self.assertEqual(datetime_to_str(60 * 60), "1970-01-01T01:00:00Z")
|
||||
self.assertEqual(datetime_to_str(60 * 60 * 24), "1970-01-02T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(0.1),
|
||||
"1970-01-01T00:00:00.100000Z")
|
||||
self.assertEqual(datetime_to_str(1),
|
||||
"1970-01-01T00:00:01Z")
|
||||
self.assertEqual(datetime_to_str(60),
|
||||
"1970-01-01T00:01:00Z")
|
||||
self.assertEqual(datetime_to_str(60 * 60),
|
||||
"1970-01-01T01:00:00Z")
|
||||
self.assertEqual(datetime_to_str(60 * 60 * 24),
|
||||
"1970-01-02T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(60 * 60 * 24 * 31),
|
||||
"1970-02-01T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(60 * 60 * 24 * 365),
|
||||
"1971-01-01T00:00:00Z")
|
||||
#
|
||||
self.assertEqual(datetime_to_str(1234567890), "2009-02-13T23:31:30Z")
|
||||
# Random other datetime
|
||||
self.assertEqual(datetime_to_str(1234567890),
|
||||
"2009-02-13T23:31:30Z")
|
||||
# Rounding issues
|
||||
self.assertEqual(datetime_to_str(0.199999),
|
||||
"1970-01-01T00:00:00.199999Z")
|
||||
@ -42,11 +49,12 @@ class TestW3cDatetime(unittest.TestCase):
|
||||
self.assertEqual(datetime_to_str(0.200001),
|
||||
"1970-01-01T00:00:00.200001Z")
|
||||
# No fractions
|
||||
self.assertEqual(datetime_to_str(100, True), "1970-01-01T00:01:40Z")
|
||||
self.assertEqual(datetime_to_str(100, True),
|
||||
"1970-01-01T00:01:40Z")
|
||||
self.assertEqual(datetime_to_str(0.2000009, True),
|
||||
"1970-01-01T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(
|
||||
0.200001, no_fractions=True), "1970-01-01T00:00:00Z")
|
||||
self.assertEqual(datetime_to_str(0.200001, no_fractions=True),
|
||||
"1970-01-01T00:00:00Z")
|
||||
|
||||
def test02_str_to_datetime(self):
|
||||
"""Reading."""
|
||||
@ -54,11 +62,10 @@ class TestW3cDatetime(unittest.TestCase):
|
||||
self.assertEqual(str_to_datetime("1970-01-01T00:00:00.000Z"), 0)
|
||||
self.assertEqual(str_to_datetime("1970-01-01T00:00:00+00:00"), 0)
|
||||
self.assertEqual(str_to_datetime("1970-01-01T00:00:00-00:00"), 0)
|
||||
self.assertEqual(str_to_datetime(
|
||||
"1970-01-01T00:00:00.000001Z"), 0.000001)
|
||||
self.assertEqual(str_to_datetime("1970-01-01T00:00:00.000001Z"), 0.000001)
|
||||
self.assertEqual(str_to_datetime("1970-01-01T00:00:00.1Z"), 0.1)
|
||||
self.assertEqual(str_to_datetime("1970-01-01T00:00:00.100000Z"), 0.1)
|
||||
#
|
||||
# Random other datetime
|
||||
self.assertEqual(str_to_datetime("2009-02-13T23:31:30Z"), 1234567890)
|
||||
|
||||
def test03_same(self):
|
||||
@ -88,15 +95,12 @@ class TestW3cDatetime(unittest.TestCase):
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-13-01")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-12-32")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T10:10:60")
|
||||
self.assertRaises(ValueError, str_to_datetime,
|
||||
"2012-11-01T10:10:59.9x")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T10:10:59.9x")
|
||||
# Valid ISO8601 but not allowed in W3C Datetime
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T01:01:01")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01 01:01:01Z")
|
||||
self.assertRaises(ValueError, str_to_datetime,
|
||||
"2012-11-01T01:01:01+0000")
|
||||
self.assertRaises(ValueError, str_to_datetime,
|
||||
"2012-11-01T01:01:01-1000")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T01:01:01+0000")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T01:01:01-1000")
|
||||
# Bad values
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-00-01T01:01:01Z")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-13-01T01:01:01Z")
|
||||
@ -105,10 +109,8 @@ class TestW3cDatetime(unittest.TestCase):
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-13-01T24:01:01Z")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-13-01T00:60:01Z")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-13-01T00:00:60Z")
|
||||
self.assertRaises(ValueError, str_to_datetime,
|
||||
"2012-11-01T01:01:01+99:00")
|
||||
self.assertRaises(ValueError, str_to_datetime,
|
||||
"2012-11-01T01:01:01-25:00")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T01:01:01+99:00")
|
||||
self.assertRaises(ValueError, str_to_datetime, "2012-11-01T01:01:01-25:00")
|
||||
|
||||
def test05_roundtrips(self):
|
||||
"""Round trips."""
|
||||
|
||||
@ -5,15 +5,16 @@ import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""Adds setUpClass an tearDownClass that create and destroy tmpdir."""
|
||||
|
||||
_tmpdir=None
|
||||
_tmpdir = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Create tmp dir to write to and check
|
||||
cls._tmpdir=tempfile.mkdtemp()
|
||||
cls._tmpdir = tempfile.mkdtemp()
|
||||
if (not os.path.isdir(cls._tmpdir)):
|
||||
raise Exception("Failed to create tempdir to use for dump tests")
|
||||
try:
|
||||
@ -39,6 +40,6 @@ class TestCase(unittest.TestCase):
|
||||
# FIXME - Hack to work on python2.6 where setUpClass is not called, will
|
||||
# FIXME - not have proper tidy as tearDownClass will not be called.
|
||||
# FIXME - Remove when 2.6 no longer supported
|
||||
if (not self._tmpdir and sys.version_info < (2,7)):
|
||||
if (not self._tmpdir and sys.version_info < (2, 7)):
|
||||
self.setUpClass()
|
||||
return(self._tmpdir)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user