帮助在网页中导航批量结果的辅助工具。
项目描述
批导航提供了一种通过提供URL链接到结果的下页、上页和编号页来在网页中导航批处理结果的方法。
它使用四个查询/POST参数来控制分批。
memo:批处理位置的底层存储索引指针的记录。
direction:指示memo是否在批处理的开始或结束。
start:外观 - 用于计算明显的位置(但请注意,由于批处理重复访问的并发性,实际偏移量可能不同 - 然而,集合不会跳过或显示项目两次。为了与保存的URL兼容,如果memo和direction都缺失,则使用start在集合中进行列表切片。
batch:控制每个批处理显示的项目数量。如果它与创建批处理时设置的默认值不同,则将出现。
除非同时传递force_start=True,否则这些值可以在请求中覆盖,这将使start参数(再次,默认为0)始终被选中。
导入
>>> from lazr.batchnavigator import BatchNavigator, ListRangeFactory >>> from zope.publisher.browser import TestRequest >>> from zope.publisher.http import HTTPCharsets >>> from zope.component import getSiteManager >>> sm = getSiteManager() >>> sm.registerAdapter(HTTPCharsets)>>> def build_request(query_string_args=None, method='GET'): ... if query_string_args is None: ... query_string = '' ... else: ... if getattr(query_string_args, 'items', None) is not None: ... query_string_args = query_string_args.items() ... query_string = "&".join( ... ["%s=%s" % (k,v) for k,v in query_string_args]) ... request = TestRequest(SERVER_URL='http://www.example.com/foo', ... method=method, ... environ={'QUERY_STRING': query_string}) ... request.processInputs() ... return request
一个示例请求对象
一些示例数据。
>>> reindeer = ['Dasher', 'Dancer', 'Prancer', 'Vixen', 'Comet', ... 'Cupid', 'Donner', 'Blitzen', 'Rudolph']
由于切片大型集合可能非常昂贵,BatchNavigator提供了一种确定批处理边缘的非切片协议。range_factory提供实现了IRangeFactory的对象,并管理此协议。ListRangeFactory是一个简单的内置实现,如果没有提供range_factory,BatchNavigator将使用它。
>>> _ = BatchNavigator(reindeer, build_request(), ... range_factory=ListRangeFactory(reindeer))
对于文档中的示例,我们让BatchNavigator隐式构建range_factory。
>>> safe_reindeer = reindeer >>> safe_reindeer_batch_navigator = BatchNavigator( ... safe_reindeer, build_request(), size=3)
lazr.batchnavigator的一个重要特性是它不倾向于对底层数据集调用len()。len()可能是一个昂贵的操作,提供的好处很少,因此此库将尽量不调用len(),除非绝对必要。为了展示这一点,我们将定义一个Python列表类型的子类,当调用len()时会崩溃。
>>> class ListWithExplosiveLen(list): ... """A list subclass that doesn't like its len() being called.""" ... def __len__(self): ... raise RuntimeError
除非另有说明,否则我们将在此测试中独家使用此列表,以验证len()只有在需要时才会调用。
>>> explosive_reindeer = ListWithExplosiveLen(reindeer) >>> reindeer_batch_navigator = BatchNavigator( ... explosive_reindeer, build_request(), size=3)
BatchNavigator实现了IBatchNavigator。我们需要在这里使用“安全”的批处理导航器,因为verifyObject调查传递给它的对象的所有方法,包括__len__。
>>> from zope.interface.verify import verifyObject >>> from lazr.batchnavigator.interfaces import IBatchNavigator>>> verifyObject(IBatchNavigator, safe_reindeer_batch_navigator) True
BatchNavigator类提供IBatchNavigatorFactory。这可以用于将批处理导航器工厂注册为实用工具,例如。
>>> from lazr.batchnavigator.interfaces import IBatchNavigatorFactory>>> verifyObject(IBatchNavigatorFactory, BatchNavigator) True
您可以要求导航器获取当前显示的结果块(例如,为了在ZPT中渲染而迭代它们)
>>> list(reindeer_batch_navigator.currentBatch()) ['Dasher', 'Dancer', 'Prancer']
您可以请求第一个、上一个、下一个和最后一个结果的链接
>>> reindeer_batch_navigator.firstBatchURL() '' >>> reindeer_batch_navigator.prevBatchURL() '' >>> reindeer_batch_navigator.nextBatchURL() 'http://www.example.com/foo?memo=3&start=3'
没有方法可以获取到最后一批的URL,除非知道整个列表的长度,所以我们将使用安全批处理导航器来演示lastBatchURL()
>>> safe_reindeer_batch_navigator.lastBatchURL() 'http://www.example.com/foo?direction=backwards&start=6'
当没有更多结果时,下一个链接将为空
>>> request = build_request({"start": "3", "batch": "20"}) >>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request) >>> last_reindeer_batch_navigator.nextBatchURL() ''
即使我们从0和批处理大小之间的某个点开始,第一个和上一个链接也应该出现
>>> request = build_request({"start": "2", "batch": "3"}) >>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
在这里,我们还可以看到批处理参数作为URL的一部分出现。这是因为请求在创建Batch对象时请求了与默认值不同的尺寸,默认情况下是5。
>>> last_reindeer_batch_navigator.firstBatchURL() 'http://www.example.com/foo?batch=3'>>> last_reindeer_batch_navigator.prevBatchURL() 'http://www.example.com/foo?batch=3&direction=backwards&memo=2'
这也可以与查询字符串中的其他值一起工作
>>> request = build_request({'fnorb': 'bar', ... 'start': '3', ... 'batch': '3'}) >>> reindeer_batch_navigator_with_qs = BatchNavigator( ... reindeer, request, size=3) >>> safe_reindeer_batch_navigator_with_qs = BatchNavigator( ... safe_reindeer, request, size=3)
在这种情况下,我们使用默认大小为“3”创建了BatchNavigator,请求的确实是每批次的那个数量,因此,我们不需要在URL中显示“批次”。
>>> reindeer_batch_navigator_with_qs.firstBatchURL() 'http://www.example.com/foo?fnorb=bar' >>> reindeer_batch_navigator_with_qs.prevBatchURL() 'http://www.example.com/foo?fnorb=bar&direction=backwards&memo=3' >>> reindeer_batch_navigator_with_qs.nextBatchURL() 'http://www.example.com/foo?fnorb=bar&memo=6&start=6'
(同样,不知道整个列表的大小,就无法获取最后一个批次。)
>>> safe_reindeer_batch_navigator_with_qs.lastBatchURL() 'http://www.example.com/foo?fnorb=bar&direction=backwards&start=6'
参数force_start允许你忽略请求中的起始值。例如,当过滤器发生变化,需要从0开始重新启动时,这非常有用。
>>> reindeer_batch_navigator_with_qs = BatchNavigator( ... reindeer, request, size=3, force_start=True) >>> reindeer_batch_navigator_with_qs.currentBatch().start 0 >>> reindeer_batch_navigator_with_qs.nextBatchURL() 'http://www.example.com/foo?fnorb=bar&memo=3&start=3' >>> reindeer[:3] == list(reindeer_batch_navigator_with_qs.currentBatch()) True
我们确保在POST操作中也会观察到URL中提供的批量参数。
>>> request = build_request({'fnorb': 'bar', ... 'start': '3', ... 'batch': '3'}, method='POST') >>> reindeer_batch_navigator_post_with_qs = BatchNavigator( ... reindeer, request)>>> reindeer_batch_navigator_post_with_qs.start 3 >>> reindeer_batch_navigator_post_with_qs.nextBatchURL() 'http://www.example.com/foo?fnorb=bar&batch=3&memo=6&start=6'
我们确保在URL中提供的多个大小和批量参数不会使应用程序崩溃。首选第一个。
>>> request = build_request( ... [('batch', '1'), ('batch', '7'), ('start', '2'), ('start', '10')]) >>> navigator = BatchNavigator(reindeer, request=request) >>> navigator.nextBatchURL() 'http://www.example.com/foo?batch=1&memo=3&start=3'
批量参数必须是正数。其他数字将被忽略,并使用默认的批量大小。
>>> from six.moves.urllib.parse import parse_qs >>> request = build_request({'batch': '0'}) >>> navigator = BatchNavigator(range(99), request=request) >>> print('batch' in parse_qs(navigator.nextBatchURL())) False>>> request = build_request({'batch': '-1'}) >>> navigator = BatchNavigator(range(99), request=request) >>> print('batch' in parse_qs(navigator.nextBatchURL())) False
空批次
你也可以创建一个没有任何项目的空批次。
>>> null_batch_navigator = BatchNavigator( ... None, build_request(), size=3) >>> null_batch_navigator.firstBatchURL() '' >>> null_batch_navigator.nextBatchURL() '' >>> null_batch_navigator.prevBatchURL() '' >>> null_batch_navigator.lastBatchURL() ''>>> null_batch_navigator = BatchNavigator( ... [], build_request(), size=3) >>> null_batch_navigator.firstBatchURL() '' >>> null_batch_navigator.nextBatchURL() '' >>> null_batch_navigator.prevBatchURL() '' >>> null_batch_navigator.lastBatchURL() ''
待办事项
当起始值超过结束值时崩溃
孤儿
重叠
支持不带 __len__ 的结果
某些结果对象没有实现 __len__,因为通常Python代码假设 __len__ 是廉价的。例如,SQLObject和Storm结果集都有这种行为,以便明确知道获取长度是一个非简单操作。
为了支持这些对象,批量会查找结果集中的 __len__。如果不存在,它将结果适配到 zope.interface.common.sequence.IFiniteSequence 并使用那个 __len__。
>>> class ExampleResultSet(object): ... def __init__(self, results): ... self.stub_results = results ... def count(self): ... # imagine this actually returned ... return len(self.stub_results) ... def __getitem__(self, ix): ... return self.stub_results[ix] # also works with slices ... def __iter__(self): ... return iter(self.stub_results) ... >>> from zope.interface import implementer >>> from zope.component import adapter, getSiteManager >>> from zope.interface.common.sequence import IFiniteSequence >>> @adapter(ExampleResultSet) ... @implementer(IFiniteSequence) ... class ExampleAdapter(ExampleResultSet): ... def __len__(self): ... return self.stub_results.count() ... >>> sm = getSiteManager() >>> sm.registerAdapter(ExampleAdapter) >>> example = ExampleResultSet(safe_reindeer) >>> example_batch_navigator = BatchNavigator( ... example, build_request(), size=3) >>> example_batch_navigator.currentBatch().total() 9
只获取所需的内容
对于批量大结果集的性能来说,也很重要,批量只获取结果的一部分,而不是访问全部。
>>> class ExampleResultSet(ExampleResultSet): ... def __init__(self, results): ... super(ExampleResultSet, self).__init__(results) ... self.getitem_history = [] ... def __getitem__(self, ix): ... self.getitem_history.append(ix) ... return super(ExampleResultSet, self).__getitem__(ix) ...>>> example = ExampleResultSet(reindeer) >>> example_batch_navigator = BatchNavigator( ... example, build_request(), size=3) >>> reindeer[:3] == list(example_batch_navigator.currentBatch()) True >>> example.getitem_history [slice(0, 4, None)]
请注意,尽管批量的大小与请求的一致,但底层列表中包含一个比必要的项目多。这是为了便于确定给定的批次是否是列表中的最后一个批次,而无需显式查找列表的长度(可能是一个昂贵的操作)。
添加回调函数
有时,当确定批次值后,需要调用某个函数。这是需要为每个批次执行后续查询的情况,而执行整个结果集的查询既不理想或过于昂贵。
回调函数必须定义两个参数。第一个是批处理导航器对象本身,第二个是当前批次。当批处理导航器构造,并确定当前批次时,回调函数只被调用一次。
>>> def print_callback(context, batch): ... for item in batch: ... print(item)>>> reindeer_batch_navigator = BatchNavigator( ... reindeer, build_request(), size=3, callback=print_callback) Dasher Dancer Prancer>>> request = build_request({"start": "3", "batch": "20"}) >>> last_reindeer_batch_navigator = BatchNavigator( ... reindeer, request=request, callback=print_callback) Vixen Comet Cupid Donner Blitzen Rudolph
最有可能的是,回调函数将绑定到一个视图类。通过将批处理导航器本身作为回调的上下文,可以添加额外的成员变量。当批处理导航器成为分页模板的上下文时,这非常有用。
>>> class ReindeerView: ... def constructReindeerFromAtoms(self, context, batch): ... # some significantly slow process ... view.built_reindeer = list(batch) ... def batchedReindeer(self): ... return BatchNavigator( ... reindeer, build_request(), size=3, ... callback=self.constructReindeerFromAtoms)>>> view = ReindeerView() >>> batch_navigator = view.batchedReindeer() >>> print(view.built_reindeer) ['Dasher', 'Dancer', 'Prancer'] >>> print(list(batch_navigator.currentBatch())) ['Dasher', 'Dancer', 'Prancer']
最大批量大小
由于批量大小在URL中公开,用户可以调整批量参数以获取更多结果。由于这可能耗尽服务器资源,因此对批量大小设置了上限。如果请求的批量参数高于此值,将引发 InvalidBatchSizeError。
>>> from lazr.batchnavigator.interfaces import InvalidBatchSizeError>>> class DemoBatchNavigator(BatchNavigator): ... max_batch_size = 5 ... >>> request = build_request({"start": "0", "batch": "20"}) >>> test_raises( ... InvalidBatchSizeError, DemoBatchNavigator, ... reindeer, request=request) Maximum for "batch" parameter is 5.
URL参数
通常,当前页面URL中传递的任何参数都会在批量导航器的链接中重新生成。一个“短暂”参数是仅与当前页面请求相关的参数,不应传递给后续请求。
在下一个批处理导航器中,页面的URL中出现了两个参数:“noisy”和“quiet”。
>>> request_parameters = { ... 'quiet': 'ssht', ... 'noisy': 'HELLO', ... }>>> request_with_parameters = build_request(request_parameters)
其中一个参数“quiet”是短暂的。还有一个短暂的参数称为“absent”,但它没有被传递到我们当前的页面请求中。
>>> def build_navigator(list): ... return BatchNavigator( ... list, request_with_parameters, size=3, ... transient_parameters=['quiet', 'absent']) >>> navigator_with_parameters = build_navigator(reindeer) >>> safe_navigator_with_parameters = build_navigator(safe_reindeer)
在这三个参数中,只有“noisy”在批量导航器生成的链接中重复。
>>> navigator_with_parameters.nextBatchURL() 'http://www.example.com/foo?noisy=HELLO&memo=3&start=3' >>> safe_navigator_with_parameters.lastBatchURL() 'http://www.example.com/foo?noisy=HELLO&direction=backwards&start=6'
瞬态参数被省略了,一开始就没有传递进来的参数也不会神奇地出现。
批量标题
批量值通常是某种对象,如错误。BatchNavigator的标题属性包含要显示的对象的描述。
>>> safe_reindeer_batch_navigator.heading 'results'
当批量中只有一个项目时,会返回标题的单数形式。
>>> navigator = BatchNavigator(['only-one'], request=request) >>> navigator.heading 'result'
(访问.heading会导致在底层列表上调用len(),这就是为什么我们必须使用安全的批量导航器。理论上,这可以优化,但实际上并没有太大意义,因为标题总是紧随底层列表的实际长度之前,例如“10个结果”。由于无论如何都会调用len(),并且其值被缓存,第二次调用len()不会影响性能。)
可以通过传递标题的单数和复数形式来设置标题。批量导航将根据批量中的总项目数返回适当的标题。
>>> navigator = BatchNavigator(safe_reindeer, request=request) >>> navigator.setHeadings('bug', 'bugs') >>> navigator.heading 'bugs'>>> navigator = BatchNavigator(['only-one'], request=request) >>> navigator.setHeadings('bug', 'bugs') >>> navigator.heading 'bug'
(清理)
>>> sm.unregisterAdapter(HTTPCharsets) True >>> sm.unregisterAdapter(ExampleAdapter) True