3 '''Marshal in and out of internal object representations
5 Internally, we use the object types defined in core.bits,
6 however we need some way of getting stuff in and out of that format
7 both for IPC, and so that people don't have to write their
8 item definitions in Python[0]. We also need to be able to talk in
9 different formats to build APIs with.
11 To do this, we're going to use a simple, data based common format that
12 should be able to be represented in several different formats (e.g.
13 python objects, json, xml etc). Bonus points for being able to use
14 off-the-shelf encoders and decoders.
16 The internal format for item classes is based is a list of dicts in
20 { 'name': 'itemname', # required
21 'depends': ['itemname2', 'itemname43', 'groupname'] # optional
22 'description': 'multi-line description of the item', # optional
23 'if': '<predicate definition>' # optional
26 { 'name': 'itemname2',
30 { 'group': 'groupname', # required
31 'contains': ['itemname43', itemname32','groupname5'] # required
32 'depends': ['itemname47', 'groupname2' ...] # optional
33 'description': 'multi-line description of the item', # optional
34 'if': '<predicate definition>' # optional
39 where all dependencies refered to must be defined in the list.
40 This is equivalent to the internal definition:
42 class itemname(bits.Item):
43 description = 'multi-line description of the item'
44 depends = (itemname2, itemname43, groupname)
45 predicate = <callable that returns True iff predicate holds over passed requirements>
47 class itemname2(bits.Item):
50 class groupname(bits.Group):
51 description = 'multi-line description of the item'
52 depends = (itemname47, groupname2)
53 contains = (itemname43, itemname32, groupname5)
54 predicate = <callable that returns True iff predicate holds over passed requirements>
56 items = [ itemname, itemname2, groupname ]
58 Item instances are represented in the same way, but the dicts can have
59 extra key/value pairs for item state and metadata. These are available
60 as as a dict as the 'data' property on the python object instances.
62 Group instances are not currently able to be marshalled; Only classes.
63 Groups should be reduced out during the process of Task creation.
65 Predicate definitions as strings are currently as defined in the digraphtools.predicate module
66 We use the PredicateContainsFactory to generate the predicates. This also allows us to marshal
67 them back and forward pretty easily from strings
69 [0] Although it's extensible inside so that you can do things like
70 write predicates in pure python, the whole system has to be usable
71 by someone that doesn't know a line of python.
74 from digraphtools.predicate import PredicateContainsFactory
76 class ItemConverter(object):
77 '''Convert items to and from Item objects
80 identifier_chrs = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_")
81 reserved_keys = set(('name','group','depends','description','contains','if'))
82 def normalise_item_name(self, name):
83 '''return a passed string that can be used as a python class name'''
85 name = filter(self.identifier_chrs.__contains__, name)
86 if name[:1].isdigit():
90 def predicate_string_to_callable(self, predicate):
91 '''turn a predicate into a callable'''
92 pf = PredicateContainsFactory()
93 pred = pf.predicate_from_string(predicate)
94 pred._predicate_string = predicate # Save for marshalling back the other way
97 def predicate_callable_to_string(self, predicate):
98 '''turn a predicate into a callable'''
99 if hasattr(predicate, '_predicate_string'):
100 return predicate._predicate_string # Restore previous knowledge. Mwahahahah
101 raise ValueError('Cannot marshal strange predicate into a string.')
103 def itemdict_to_group(self, itemdict):
104 '''return a Group subclass from an item dict datastructure
105 This does not unroll dependencies or group contents from strings into classes
106 pre: itemdict is valid
108 assert not itemdict.has_key('name')
109 name = self.normalise_item_name(itemdict['group'])
110 attrs = dict(contains=itemdict['contains'])
111 if itemdict.has_key('depends'): attrs['depends'] = tuple(itemdict['depends'])
112 if itemdict.has_key('description'): attrs['description'] = itemdict['description']
113 if itemdict.has_key('if'): attrs['predicate'] = self.predicate_string_to_callable(itemdict['if'])
114 return type.__new__(type, name, (core.bits.Group,), attrs)
116 def itemdict_to_item_class(self, itemdict):
117 '''return an Item subclass from an item dict datastructure
118 This does not unroll item dependencies from strings into classes
120 pre: itemdict is valid
122 if itemdict.has_key('group'):
123 return self.itemdict_to_group(itemdict)
125 name = self.normalise_item_name(itemdict['name'])
126 if name == 'TaskComplete':
127 itemsuper = core.bits.TaskComplete
129 itemsuper = core.bits.Item
131 if itemdict.has_key('depends'): attrs['depends'] = tuple(itemdict['depends'])
132 if itemdict.has_key('description'): attrs['description'] = itemdict['description']
133 if itemdict.has_key('if'): attrs['predicate'] = self.predicate_string_to_callable(itemdict['if'])
134 return type.__new__(type, name, (itemsuper,), attrs)
136 def itemdict_to_item_instance(self, itemdict):
137 cl = self.itemdict_to_item_class(itemdict)
138 data = dict((k,v) for k,v in itemdict.items() if k not in self.reserved_keys)
141 def itemclass_to_itemdict(self, item):
142 '''return an item dict datastructure from an Item or Group subclass'''
143 if issubclass(item,core.bits.Group):
144 itemdict = dict(group=item.__name__, contains=[c.__name__ for c in item.contains])
146 itemdict = dict(name=item.__name__)
147 if item.depends: itemdict['depends'] = list(d.__name__ for d in item.depends)
148 if item.description: itemdict['description'] = item.description
149 if item.predicate != core.bits.BaseItem.predicate:
150 # This might fail if someone has put their own callable in as a predicate
151 # That's okay; it just means they can't marshal their classes back to json
152 itemdict['if'] = self.predicate_callable_to_string(item.predicate)
155 def item_to_itemdict(self, item):
156 '''return an item dict datastructure from an Item instance
157 Note: Does not work on groups and does not convert predicates
159 assert not isinstance(item, core.bits.Group)
160 itemdict = dict(name=item.name)
161 itemdict.update(dict((k,v) for k,v in item.data.items() if k not in self.reserved_keys))
162 if item.description: itemdict['description'] = item.description
163 if len(item.depends):
164 itemdict['depends'] = [d.name for d in item.depends]
167 class TaskConverter(ItemConverter):
168 def taskdict_to_task(self, taskdict):
169 # turn the items into instances
170 items = map(self.itemdict_to_item_instance, taskdict['items'])
172 # reference them to each other correctly
173 item_by_name = dict((item.name,item) for item in items)
175 item.depends = tuple(item_by_name[dep] for dep in item.depends)
178 metadata = taskdict['metadata']
179 goal = item_by_name['TaskComplete']
180 requirements = metadata['requirements']
181 uuid = metadata['uuid']
182 return core.bits.Task(items, requirements, goal, uuid, metadata)