44and a ``public_key`` used for verifying that a certificate(s) was properly signed.
55"""
66import json
7- import string
87import logging
8+ import string
9+ from contextlib import contextmanager
910
1011import mptt .models
1112from django .core .management import call_command
13+ from django .db import connection
1214from django .db import models
1315from django .db import transaction
16+ from django .db .utils import OperationalError
1417from django .utils import timezone
1518
1619from .fields .crypto import Key
2427from morango .errors import CertificateSignatureInvalid
2528from morango .errors import NonceDoesNotExist
2629from morango .errors import NonceExpired
27- from morango .utils import _assert
28- from django .db import transaction , connection
2930from morango .sync .backends .utils import load_backend
30- from contextlib import contextmanager
31- from django . db . utils import OperationalError
31+ from morango . utils import _assert
32+
3233
3334class Certificate (mptt .models .MPTTModel , UUIDModelMixin ):
3435
@@ -360,45 +361,89 @@ def get_description(self, params):
360361
361362
362363class Filter (object ):
363- def __init__ (self , template , params = {}):
364- # ensure params have been deserialized
365- if isinstance (params , str ):
366- params = json .loads (params )
367- self ._template = template
368- self ._params = params
369- self ._filter_string = string .Template (template ).safe_substitute (params )
370- self ._filter_tuple = tuple (self ._filter_string .split ()) or ("" ,)
364+ def __init__ (self , filter_str , params = None ):
365+ """
366+ :param filter_str: The partition filter string, which may have multiple separated by newlines
367+ :type filter_str: str
368+ :param params: DEPRECATED: USE Filter.from_template() INSTEAD
369+ :type params: dict|str
370+ """
371+ if params is not None :
372+ logging .warning ("DEPRECATED: Constructing a filter with a template and params is deprecated. Use Filter.from_template() instead" )
373+ filter_str = str (Filter .from_template (filter_str , params = params ))
374+
375+ self ._filter_tuple = tuple (filter_str .split ()) or ("" ,)
371376
372377 def is_subset_of (self , other ):
373- for partition in self ._filter_tuple :
374- if not partition .startswith (other ._filter_tuple ):
378+ """
379+ :param other: The other Filter
380+ :type other: Filter
381+ :return: A boolean on whether this Filter is captured within the other Filter
382+ :rtype: bool
383+ """
384+ for partition in self :
385+ if not other .contains_partition (partition ):
375386 return False
376387 return True
377388
378389 def contains_partition (self , partition ):
390+ """Returns True if the partition starts with as least one of the partitions in this Filter"""
379391 return partition .startswith (self ._filter_tuple )
380392
393+ def contains_exact_partition (self , partition ):
394+ """Returns True if the partition exactly matches one of the partitions in this Filter"""
395+ return partition in self ._filter_tuple
396+
397+ def copy (self ):
398+ return Filter (str (self ))
399+
381400 def __le__ (self , other ):
401+ """Returns True if this Filter is a subset of the other"""
382402 return self .is_subset_of (other )
383403
384404 def __eq__ (self , other ):
405+ """Returns True if this Filter has exactly the same partitions as the other"""
385406 if other is None :
386407 return False
387- for partition in self . _filter_tuple :
388- if partition not in other ._filter_tuple :
408+ for partition in self :
409+ if not other .contains_exact_partition ( partition ) :
389410 return False
390- for partition in other . _filter_tuple :
391- if partition not in self ._filter_tuple :
411+ for partition in other :
412+ if not self .contains_exact_partition ( partition ) :
392413 return False
393414 return True
394415
395416 def __contains__ (self , partition ):
417+ """
418+ Performs a 'startswith' comparison on the partition, determining whether it matches or
419+ is a subset of any partition in this Filter
420+
421+ :param partition: str
422+ :return: A boolean
423+ :rtype: bool
424+ """
396425 return self .contains_partition (partition )
397426
398427 def __add__ (self , other ):
399- return Filter (self ._filter_string + "\n " + other ._filter_string )
428+ """
429+ The Filter's addition operator overload
430+ :param other: Filter or None
431+ :type other: Filter|None
432+ :return: The combined Filter
433+ :rtype: Filter
434+ """
435+ if other is None :
436+ return self
437+ # create a list of partition filters, deduplicating them between the two filter objects
438+ partitions = []
439+ partitions .extend (p for p in self if p )
440+ partitions .extend (p for p in other if p and p not in partitions )
441+ return Filter ("\n " .join (partitions ))
400442
401443 def __iter__ (self ):
444+ """
445+ :rtype: tuple[str]
446+ """
402447 return iter (self ._filter_tuple )
403448
404449 def __str__ (self ):
@@ -407,13 +452,48 @@ def __str__(self):
407452 def __len__ (self ):
408453 return len (self ._filter_tuple )
409454
455+ @classmethod
456+ def add (cls , filter_a , filter_b ):
457+ """
458+ The Filter's addition operator overload is already defensive against None being the
459+ right-hand operand, but this method is defensive against None being the left-hand operand
460+
461+ :param filter_a: A Filter or None
462+ :type filter_a: Filter|None
463+ :param filter_b: A Filter or None
464+ :type filter_b: Filter|None
465+ :return: The combined Filter or None
466+ :rtype: Filter|None
467+ """
468+ if filter_a is None :
469+ return filter_b
470+ return filter_a + filter_b
471+
472+ @classmethod
473+ def from_template (cls , template , params = None ):
474+ """
475+ Create a filter from a string template, which may have params that will be replaced with
476+ values passed to `params`
477+
478+ :param template: The partition filter template
479+ :type template: str
480+ :param params: The param dictionary or JSON object string
481+ :type params: dict|str
482+ :return: The filter with params replaced
483+ :rtype: Filter
484+ """
485+ if isinstance (params , str ):
486+ params = json .loads (params )
487+ params = params or {}
488+ return Filter (string .Template (template ).safe_substitute (params ))
489+
410490
411491class Scope (object ):
412492 def __init__ (self , definition , params ):
413493 # turn the scope definition filter templates into Filter objects
414- rw_filter = Filter (definition .read_write_filter_template , params )
415- self .read_filter = rw_filter + Filter (definition .read_filter_template , params )
416- self .write_filter = rw_filter + Filter (definition .write_filter_template , params )
494+ rw_filter = Filter . from_template (definition .read_write_filter_template , params )
495+ self .read_filter = rw_filter + Filter . from_template (definition .read_filter_template , params )
496+ self .write_filter = rw_filter + Filter . from_template (definition .write_filter_template , params )
417497
418498 def is_subset_of (self , other ):
419499 if not self .read_filter .is_subset_of (other .read_filter ):
0 commit comments