synapse 2.176.0__py311-none-any.whl → 2.177.0__py311-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synapse might be problematic. Click here for more details.

Files changed (70) hide show
  1. synapse/axon.py +24 -9
  2. synapse/cortex.py +329 -168
  3. synapse/cryotank.py +46 -37
  4. synapse/datamodel.py +17 -4
  5. synapse/exc.py +19 -0
  6. synapse/lib/agenda.py +7 -13
  7. synapse/lib/auth.py +1520 -0
  8. synapse/lib/cell.py +255 -53
  9. synapse/lib/grammar.py +5 -0
  10. synapse/lib/hive.py +24 -3
  11. synapse/lib/hiveauth.py +6 -32
  12. synapse/lib/layer.py +7 -4
  13. synapse/lib/link.py +21 -17
  14. synapse/lib/lmdbslab.py +149 -0
  15. synapse/lib/modelrev.py +1 -1
  16. synapse/lib/schemas.py +136 -0
  17. synapse/lib/storm.py +61 -29
  18. synapse/lib/stormlib/aha.py +1 -1
  19. synapse/lib/stormlib/auth.py +185 -10
  20. synapse/lib/stormlib/cortex.py +16 -5
  21. synapse/lib/stormlib/gen.py +80 -0
  22. synapse/lib/stormlib/model.py +55 -0
  23. synapse/lib/stormlib/modelext.py +60 -0
  24. synapse/lib/stormlib/tabular.py +212 -0
  25. synapse/lib/stormtypes.py +14 -1
  26. synapse/lib/trigger.py +1 -1
  27. synapse/lib/version.py +2 -2
  28. synapse/lib/view.py +55 -28
  29. synapse/models/base.py +7 -0
  30. synapse/models/biz.py +4 -0
  31. synapse/models/files.py +8 -1
  32. synapse/models/inet.py +8 -0
  33. synapse/tests/files/changelog/model_2.176.0_16ee721a6b7221344eaf946c3ab4602dda546b1a.yaml.gz +0 -0
  34. synapse/tests/files/changelog/model_2.176.0_2a25c58bbd344716cd7cbc3f4304d8925b0f4ef2.yaml.gz +0 -0
  35. synapse/tests/test_axon.py +7 -4
  36. synapse/tests/test_cortex.py +127 -82
  37. synapse/tests/test_cryotank.py +4 -4
  38. synapse/tests/test_datamodel.py +7 -0
  39. synapse/tests/test_lib_agenda.py +7 -0
  40. synapse/tests/{test_lib_hiveauth.py → test_lib_auth.py} +314 -11
  41. synapse/tests/test_lib_cell.py +161 -8
  42. synapse/tests/test_lib_httpapi.py +18 -14
  43. synapse/tests/test_lib_layer.py +33 -33
  44. synapse/tests/test_lib_link.py +42 -1
  45. synapse/tests/test_lib_lmdbslab.py +68 -0
  46. synapse/tests/test_lib_nexus.py +4 -4
  47. synapse/tests/test_lib_node.py +0 -7
  48. synapse/tests/test_lib_storm.py +45 -0
  49. synapse/tests/test_lib_stormlib_aha.py +1 -2
  50. synapse/tests/test_lib_stormlib_auth.py +21 -0
  51. synapse/tests/test_lib_stormlib_cortex.py +12 -12
  52. synapse/tests/test_lib_stormlib_gen.py +99 -0
  53. synapse/tests/test_lib_stormlib_model.py +108 -0
  54. synapse/tests/test_lib_stormlib_modelext.py +64 -0
  55. synapse/tests/test_lib_stormlib_tabular.py +226 -0
  56. synapse/tests/test_lib_stormsvc.py +4 -1
  57. synapse/tests/test_lib_stormtypes.py +10 -0
  58. synapse/tests/test_model_base.py +3 -0
  59. synapse/tests/test_model_biz.py +3 -0
  60. synapse/tests/test_model_files.py +12 -2
  61. synapse/tests/test_model_inet.py +24 -0
  62. synapse/tests/test_tools_changelog.py +196 -0
  63. synapse/tests/test_tools_healthcheck.py +4 -3
  64. synapse/tests/utils.py +1 -1
  65. synapse/tools/changelog.py +774 -15
  66. {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/METADATA +3 -3
  67. {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/RECORD +70 -64
  68. {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/WHEEL +1 -1
  69. {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/LICENSE +0 -0
  70. {synapse-2.176.0.dist-info → synapse-2.177.0.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,13 @@
1
1
  import os
2
2
  import re
3
3
  import sys
4
+ import copy
5
+ import gzip
4
6
  import pprint
5
7
  import asyncio
6
8
  import argparse
7
9
  import datetime
10
+ import tempfile
8
11
  import textwrap
9
12
  import traceback
10
13
  import subprocess
@@ -12,10 +15,14 @@ import collections
12
15
 
13
16
  import regex
14
17
 
18
+ import synapse.exc as s_exc
15
19
  import synapse.common as s_common
20
+ import synapse.cortex as s_cortex
16
21
 
17
22
  import synapse.lib.output as s_output
23
+ import synapse.lib.autodoc as s_autodoc
18
24
  import synapse.lib.schemas as s_schemas
25
+ import synapse.lib.version as s_version
19
26
 
20
27
  defstruct = (
21
28
  ('type', None),
@@ -25,13 +32,399 @@ defstruct = (
25
32
 
26
33
  SKIP_FILES = (
27
34
  '.gitkeep',
35
+ 'modelrefs',
28
36
  )
29
37
 
30
38
  version_regex = r'^v[0-9]\.[0-9]+\.[0-9]+((a|b|rc)[0-9]*)?$'
31
- def gen(opts: argparse.Namespace,
32
- outp: s_output.OutPut):
33
- if opts.verbose:
34
- outp.printf(f'{opts=}')
39
+
40
+ def _getCurrentCommit(outp: s_output.OutPut) -> str | None:
41
+ try:
42
+ ret = subprocess.run(['git', 'rev-parse', 'HEAD'],
43
+ capture_output=True,
44
+ timeout=15,
45
+ check=False,
46
+ text=True,
47
+ )
48
+ except Exception as e:
49
+ outp.printf(f'Error grabbing commit: {e}')
50
+ return
51
+ else:
52
+ commit = ret.stdout.strip()
53
+ assert commit
54
+ return commit
55
+
56
+ async def _getCurrentModl(outp: s_output.OutPut) -> dict:
57
+ with tempfile.TemporaryDirectory() as dirn:
58
+ conf = {'health:sysctl:checks': False}
59
+ async with await s_cortex.Cortex.anit(conf=conf, dirn=dirn) as core:
60
+ modl = await core.getModelDict()
61
+ # Reserialize modl so its consistent with the model on disk
62
+ modl = s_common.yamlloads(s_common.yamldump(modl))
63
+ return modl
64
+
65
+
66
+ class ModelDiffer:
67
+ def __init__(self, current_model: dict, reference_model: dict):
68
+ self.cur_model = current_model
69
+ self.ref_model = reference_model
70
+ self.changes = {}
71
+
72
+ self.cur_iface_to_allifaces = collections.defaultdict(list)
73
+ for iface, info in self.cur_model.get('interfaces').items():
74
+ self.cur_iface_to_allifaces[iface] = [iface]
75
+ q = collections.deque(info.get('interfaces', ()))
76
+ while q:
77
+ _iface = q.popleft()
78
+ if _iface in self.cur_iface_to_allifaces[iface]:
79
+ continue
80
+ self.cur_iface_to_allifaces[iface].append(_iface)
81
+ q.extend(self.cur_model.get('interfaces').get(_iface).get('interfaces', ()))
82
+
83
+ self.cur_type2iface = collections.defaultdict(list)
84
+
85
+ for _type, tnfo in self.cur_model.get('types').items():
86
+ for iface in tnfo.get('info').get('interfaces', ()):
87
+ ifaces = self.cur_iface_to_allifaces[iface]
88
+ for _iface in ifaces:
89
+ if _iface not in self.cur_type2iface[_type]:
90
+ self.cur_type2iface[_type].append(_iface)
91
+
92
+ def _compareEdges(self, curv, oldv, outp: s_output.OutPut) -> dict:
93
+ changes = {}
94
+ if curv == oldv:
95
+ return changes
96
+
97
+ # Flatten the edges into structures that can be handled
98
+ _curv = {tuple(item[0]): item[1] for item in curv}
99
+ _oldv = {tuple(item[0]): item[1] for item in oldv}
100
+
101
+ curedges = set(_curv.keys())
102
+ oldedges = set(_oldv.keys())
103
+
104
+ new_edges = curedges - oldedges
105
+ del_edges = oldedges - curedges # This should generally not happen...
106
+ assert len(del_edges) == 0, 'A edge was removed from the data model!'
107
+
108
+ if new_edges:
109
+ changes['new_edges'] = {k: _curv.get(k) for k in new_edges}
110
+
111
+ updated_edges = collections.defaultdict(dict)
112
+ for edge, curinfo in _curv.items():
113
+ if edge in new_edges:
114
+ continue
115
+ oldinfo = _oldv.get(edge)
116
+ if curinfo == oldinfo:
117
+ continue
118
+
119
+ # TODO - Support changes to the edges?
120
+ assert False, f'A change was found for the edge: {edge}'
121
+
122
+ if updated_edges:
123
+ changes['updated_edges'] = dict(updated_edges)
124
+
125
+ return changes
126
+
127
+ def _compareForms(self, curv, oldv, outp: s_output.OutPut) -> dict:
128
+ changes = {}
129
+ if curv == oldv:
130
+ return changes
131
+
132
+ curforms = set(curv.keys())
133
+ oldforms = set(oldv.keys())
134
+
135
+ new_forms = curforms - oldforms
136
+ del_forms = oldforms - curforms # This should generally not happen...
137
+ assert len(del_forms) == 0, 'A form was removed from the data model!'
138
+
139
+ if new_forms:
140
+ changes['new_forms'] = {k: curv.get(k) for k in new_forms}
141
+
142
+ updated_forms = collections.defaultdict(dict)
143
+ for form, curinfo in curv.items():
144
+ if form in new_forms:
145
+ continue
146
+ oldinfo = oldv.get(form)
147
+ if curinfo == oldinfo:
148
+ continue
149
+
150
+ # Check for different properties
151
+ nprops = curinfo.get('props')
152
+ oprops = oldinfo.get('props')
153
+
154
+ new_props = set(nprops.keys()) - set(oprops.keys())
155
+ del_props = set(oprops.keys()) - set(nprops.keys()) # This should generally not happen...
156
+ assert len(del_props) == 0, 'A form was removed from the data model!'
157
+
158
+ if new_props:
159
+ updated_forms[form]['new_properties'] = {prop: nprops.get(prop) for prop in new_props}
160
+ np_noiface = {}
161
+ ifaces = self.cur_type2iface[form]
162
+
163
+ for prop in new_props:
164
+ # TODO record raw new_props to make bulk edits possible
165
+ is_ifaceprop = False
166
+ for iface in ifaces:
167
+ # Is the prop in the new_interfaces or updated_interfaces lists?
168
+ new_iface = self.changes.get('interfaces').get('new_interfaces', {}).get(iface)
169
+ if new_iface and prop in new_iface.get('props', {}):
170
+ is_ifaceprop = True
171
+ break
172
+
173
+ upt_iface = self.changes.get('interfaces').get('updated_interfaces', {}).get(iface)
174
+ if upt_iface and prop in upt_iface.get('new_properties', {}):
175
+ is_ifaceprop = True
176
+ break
177
+
178
+ if is_ifaceprop:
179
+ continue
180
+
181
+ np_noiface[prop] = nprops.get(prop)
182
+
183
+ if np_noiface:
184
+ updated_forms[form]['new_properties_no_interfaces'] = np_noiface
185
+
186
+ updated_props = {}
187
+ updated_props_noiface = {}
188
+
189
+ deprecated_props = {}
190
+ deprecated_props_noiface = {}
191
+
192
+ for prop, cpinfo in nprops.items():
193
+ if prop in new_props:
194
+ continue
195
+ opinfo = oprops.get(prop)
196
+ if cpinfo == opinfo:
197
+ continue
198
+
199
+ # Deprecation has a higher priority than updated type information
200
+ if cpinfo.get('deprecated') and not opinfo.get('deprecated'):
201
+ # A deprecated property could be present on an updated iface
202
+ deprecated_props[prop] = cpinfo
203
+
204
+ is_ifaceprop = False
205
+ for iface in self.cur_type2iface[form]:
206
+ upt_iface = self.changes.get('interfaces').get('updated_interfaces', {}).get(iface)
207
+ if upt_iface and prop in upt_iface.get('deprecated_properties', {}):
208
+ is_ifaceprop = True
209
+ break
210
+ if is_ifaceprop:
211
+ continue
212
+
213
+ deprecated_props_noiface[prop] = cpinfo
214
+ continue
215
+
216
+ # Check if type change happened, we'll want to document that.
217
+ ctyp = cpinfo.get('type')
218
+ otyp = opinfo.get('type')
219
+ if ctyp == otyp:
220
+ continue
221
+
222
+ updated_props[prop] = {'new_type': ctyp, 'old_type': otyp}
223
+ is_ifaceprop = False
224
+ for iface in self.cur_type2iface[form]:
225
+ upt_iface = self.changes.get('interfaces').get('updated_interfaces', {}).get(iface)
226
+ if upt_iface and prop in upt_iface.get('updated_properties', {}):
227
+ is_ifaceprop = True
228
+ break
229
+ if is_ifaceprop:
230
+ continue
231
+ updated_props_noiface[prop] = {'new_type': ctyp, 'old_type': otyp}
232
+
233
+ if updated_props:
234
+ updated_forms[form]['updated_properties'] = updated_props
235
+
236
+ if updated_props_noiface:
237
+ updated_forms[form]['updated_properties_no_interfaces'] = updated_props_noiface
238
+
239
+ if deprecated_props:
240
+ updated_forms[form]['deprecated_properties'] = deprecated_props
241
+
242
+ if deprecated_props_noiface:
243
+ updated_forms[form]['deprecated_properties_no_interfaces'] = deprecated_props_noiface
244
+
245
+ if updated_forms:
246
+ changes['updated_forms'] = dict(updated_forms)
247
+
248
+ return changes
249
+
250
+ def _compareIfaces(self, curv, oldv, outp: s_output.OutPut) -> dict:
251
+ changes = {}
252
+ if curv == oldv:
253
+ return changes
254
+
255
+ curfaces = set(curv.keys())
256
+ oldfaces = set(oldv.keys())
257
+
258
+ new_faces = curfaces - oldfaces
259
+ del_faces = oldfaces - curfaces # This should generally not happen...
260
+ assert len(del_faces) == 0, 'An interface was removed from the data model!'
261
+
262
+ if new_faces:
263
+ nv = {}
264
+ for iface in new_faces:
265
+ k = copy.deepcopy(curv.get(iface))
266
+ # Rewrite props into a dictionary for easier lookup later
267
+ k['props'] = {item[0]: {'type': item[1], 'props': item[2]} for item in k['props']}
268
+ nv[iface] = k
269
+
270
+ changes['new_interfaces'] = nv
271
+
272
+ updated_interfaces = collections.defaultdict(dict)
273
+
274
+ for iface, curinfo in curv.items():
275
+ if iface in new_faces:
276
+ continue
277
+ oldinfo = oldv.get(iface)
278
+
279
+ # Did the interface inheritance change?
280
+ if curinfo.get('interfaces') != oldinfo.get('interfaces'):
281
+ updated_interfaces[iface] = {'updated_interfaces': {'curv': curinfo.get('interfaces'),
282
+ 'oldv': oldinfo.get('interfaces')}}
283
+ # Did the interface have a property definition change?
284
+ nprops = curinfo.get('props')
285
+ oprops = oldinfo.get('props')
286
+
287
+ # Convert props to dictionary
288
+ nprops = {item[0]: {'type': item[1], 'props': item[2]} for item in nprops}
289
+ oprops = {item[0]: {'type': item[1], 'props': item[2]} for item in oprops}
290
+
291
+ new_props = set(nprops.keys()) - set(oprops.keys())
292
+ del_props = set(oprops.keys()) - set(nprops.keys()) # This should generally not happen...
293
+ assert len(del_props) == 0, f'A prop was removed from the iface {iface}'
294
+
295
+ if new_props:
296
+ updated_interfaces[iface]['new_properties'] = {prop: nprops.get(prop) for prop in new_props}
297
+
298
+ updated_props = {}
299
+ deprecated_props = {}
300
+ for prop, cpinfo in nprops.items():
301
+ if prop in new_props:
302
+ continue
303
+ opinfo = oprops.get(prop)
304
+ if cpinfo == opinfo:
305
+ continue
306
+
307
+ if cpinfo.get('props').get('deprecated') and not opinfo.get('props').get('deprecated'):
308
+ deprecated_props[prop] = cpinfo
309
+ continue
310
+
311
+ # Check if type change happened, we'll want to document that.
312
+ ctyp = cpinfo.get('type')
313
+ otyp = opinfo.get('type')
314
+ if ctyp == otyp:
315
+ continue
316
+
317
+ updated_props[prop] = {'new_type': ctyp, 'old_type': otyp}
318
+ if updated_props:
319
+ updated_interfaces[iface]['updated_properties'] = updated_props
320
+
321
+ if deprecated_props:
322
+ updated_interfaces[iface]['deprecated_properties'] = deprecated_props
323
+
324
+ changes['updated_interfaces'] = dict(updated_interfaces)
325
+
326
+ return changes
327
+
328
+ def _compareTagprops(self, curv, oldv, outp: s_output.OutPut) -> dict:
329
+ changes = {}
330
+ if curv == oldv:
331
+ return changes
332
+ raise NotImplementedError('_compareTagprops')
333
+
334
+ def _compareTypes(self, curv, oldv, outp: s_output.OutPut) -> dict:
335
+ changes = {}
336
+ if curv == oldv:
337
+ return changes
338
+
339
+ curtypes = set(curv.keys())
340
+ oldtypes = set(oldv.keys())
341
+
342
+ new_types = curtypes - oldtypes
343
+ del_types = oldtypes - curtypes # This should generally not happen...
344
+ assert len(del_types) == 0, 'A type was removed from the data model!'
345
+
346
+ if new_types:
347
+ changes['new_types'] = {k: curv.get(k) for k in new_types}
348
+
349
+ updated_types = collections.defaultdict(dict)
350
+ deprecated_types = collections.defaultdict(dict)
351
+
352
+ for _type, curinfo in curv.items():
353
+ if _type in new_types:
354
+ continue
355
+ oldinfo = oldv.get(_type)
356
+ if curinfo == oldinfo:
357
+ continue
358
+
359
+ cnfo = curinfo.get('info')
360
+ onfo = oldinfo.get('info')
361
+
362
+ if cnfo.get('deprecated') and not onfo.get('deprecated'):
363
+ deprecated_types[_type] = curinfo
364
+ continue
365
+
366
+ if cnfo.get('interfaces') != onfo.get('interfaces'):
367
+ updated_types[_type]['updated_interfaces'] = {'curv': cnfo.get('interfaces'),
368
+ 'oldv': onfo.get('interfaces'),
369
+ }
370
+
371
+ if curinfo.get('opts') != oldinfo.get('opts'):
372
+ updated_types[_type]['updated_opts'] = {'curv': curinfo.get('opts'),
373
+ 'oldv': oldinfo.get('opts'),
374
+ }
375
+
376
+ if updated_types:
377
+ changes['updated_types'] = dict(updated_types)
378
+
379
+ if deprecated_types:
380
+ changes['deprecated_types'] = dict(deprecated_types)
381
+
382
+ return changes
383
+
384
+ def _compareUnivs(self, curv, oldv, outp: s_output.OutPut) -> dict:
385
+ changes = {}
386
+ if curv == oldv:
387
+ return changes
388
+ raise NotImplementedError('_compareUnivs')
389
+
390
+ def diffModl(self, outp: s_output.OutPut) -> dict | None:
391
+ if self.changes:
392
+ return self.changes
393
+
394
+ # These are order sensitive due to interface knowledge being required in order
395
+ # to deconflict downstream changes on forms.
396
+ known_keys = {
397
+ 'interfaces': self._compareIfaces,
398
+ 'types': self._compareTypes,
399
+ 'forms': self._compareForms,
400
+ 'tagprops': self._compareTagprops,
401
+ 'edges': self._compareEdges,
402
+ 'univs': self._compareUnivs,
403
+ }
404
+
405
+ all_keys = set(self.cur_model.keys()).union(self.ref_model.keys())
406
+
407
+ for key, func in known_keys.items():
408
+ self.changes[key] = func(self.cur_model.get(key),
409
+ self.ref_model.get(key),
410
+ outp)
411
+ all_keys.remove(key)
412
+
413
+ if all_keys:
414
+ outp.printf(f'ERROR: Unknown model key found: {all_keys}')
415
+ return
416
+
417
+ return self.changes
418
+
419
+ def _getModelFile(fp: str) -> dict | None:
420
+ with s_common.genfile(fp) as fd:
421
+ bytz = fd.read()
422
+ large_bytz = gzip.decompress(bytz)
423
+ ref_modl = s_common.yamlloads(large_bytz)
424
+ return ref_modl
425
+
426
+ async def gen(opts: argparse.Namespace,
427
+ outp: s_output.OutPut):
35
428
 
36
429
  name = opts.name
37
430
  if name is None:
@@ -70,10 +463,269 @@ def gen(opts: argparse.Namespace,
70
463
 
71
464
  return 0
72
465
 
73
- def format(opts: argparse.Namespace,
466
+ def _gen_model_rst(version, model_ref, changes, current_model, outp: s_output.OutPut, width=80) -> s_autodoc.RstHelp:
467
+ rst = s_autodoc.RstHelp()
468
+ rst.addHead(f'{version} Model Updates', link=f'.. _{model_ref}:')
469
+ if new_interfaces := changes.get('interfaces').get('new_interfaces'):
470
+ rst.addHead('New Interfaces', lvl=1)
471
+ for interface, info in new_interfaces.items():
472
+ rst.addLines(f'``{interface}``')
473
+ rst.addLines(*textwrap.wrap(info.get('doc'), initial_indent=' ', subsequent_indent=' ',
474
+ width=width))
475
+ rst.addLines('\n')
476
+
477
+ # Deconflict new_forms vs new_types -> do not add types which appear in new_forms.
478
+ new_forms = changes.get('forms').get('new_forms', {})
479
+ new_types = changes.get('types').get('new_types', {})
480
+ types_to_document = {k: v for k, v in new_types.items() if k not in new_forms}
481
+
482
+ if types_to_document:
483
+ rst.addHead('New Types', lvl=1)
484
+ for _type, info in types_to_document.items():
485
+ rst.addLines(f'``{_type}``')
486
+ rst.addLines(*textwrap.wrap(info.get('info').get('doc'), initial_indent=' ', subsequent_indent=' ',
487
+ width=width))
488
+ rst.addLines('\n')
489
+
490
+ if new_forms:
491
+ rst.addHead('New Forms', lvl=1)
492
+ for form, info in new_forms.items():
493
+ rst.addLines(f'``{form}``')
494
+ # Pull the form doc from the current model directly. In the event of an existing
495
+ # type being turned into a form + then reindexed, it would not show up in the
496
+ # type diff, so we can't rely on the doc being present there.
497
+ doc = current_model.get('types').get(form).get('info').get('doc')
498
+ rst.addLines(*textwrap.wrap(doc, initial_indent=' ', subsequent_indent=' ',
499
+ width=width))
500
+ rst.addLines('\n')
501
+
502
+ # Check for new properties
503
+ updated_forms = changes.get('forms').get('updated_forms', {})
504
+ new_props = []
505
+ for form, info in updated_forms.items():
506
+ if 'new_properties' in info:
507
+ new_props.append((form, info))
508
+ if new_props:
509
+ rst.addHead('New Properties', lvl=1)
510
+ new_props.sort(key=lambda x: x[0])
511
+ for form, info in new_props:
512
+ rst.addLines(f'``{form}``')
513
+ new_form_props = list(info.get('new_properties').items())
514
+ if len(new_form_props) > 1:
515
+ rst.addLines(' The form had the following properties added to it:', '\n')
516
+ new_form_props.sort(key=lambda x: x[0])
517
+ for name, info in new_form_props:
518
+ lines = [
519
+ f' ``{name}``',
520
+ *textwrap.wrap(info.get('doc'), initial_indent=' ', subsequent_indent=' ',
521
+ width=width),
522
+ '\n'
523
+ ]
524
+ rst.addLines(*lines)
525
+
526
+ else:
527
+ name, info = new_form_props[0]
528
+ lines = [
529
+ ' The form had the following property added to it:',
530
+ '\n'
531
+ f' ``{name}``',
532
+ *textwrap.wrap(info.get('doc'), initial_indent=' ', subsequent_indent=' ',
533
+ width=width),
534
+ '\n'
535
+ ]
536
+ rst.addLines(*lines)
537
+
538
+ # Updated interfaces
539
+ if updated_interfaces := changes.get('interfaces').get('updated_interfaces', {}):
540
+ upd_ifaces = list(updated_interfaces.items())
541
+ upd_ifaces.sort(key=lambda x: x[0])
542
+ rst.addHead('Updated Interfaces', lvl=1)
543
+ for iface, info in upd_ifaces:
544
+ lines = [f'``{iface}``',
545
+ ]
546
+ for key, valu in sorted(info.items(), key=lambda x: x[0]):
547
+ if key == 'deprecated_properties':
548
+ for prop, pnfo in sorted(valu.items(), key=lambda x: x[0]):
549
+ mesg = f'The interface property ``{prop}`` has been deprecated.'
550
+ lines.extend(textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
551
+ width=width))
552
+ lines.append('\n')
553
+ elif key == 'new_properties':
554
+ for prop, pnfo in sorted(valu.items(), key=lambda x: x[0]):
555
+ mesg = f'The property ``{prop}`` has been added to the interface.'
556
+ lines.extend(textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
557
+ width=width))
558
+ lines.append('\n')
559
+ elif key == 'updated_properties':
560
+ for prop, pnfo in sorted(valu.items(), key=lambda x: x[0]):
561
+ mesg = f'The property ``{prop}`` has been modified from {pnfo.get("old_type")}' \
562
+ f' to {pnfo.get("new_type")}.'
563
+ lines.extend(textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
564
+ width=width))
565
+ lines.append('\n')
566
+ else: # pragma: no cover
567
+ outp.printf(f'Unknown key: {key=} {valu=}')
568
+ raise s_exc.SynErr(mesg=f'Unknown updated interface key: {key=} {valu=}')
569
+ rst.addLines(*lines)
570
+
571
+ # Updated types
572
+ if updated_types := changes.get('types').get('updated_types', {}):
573
+ upd_types = list(updated_types.items())
574
+ upd_types.sort(key=lambda x: x[0])
575
+ rst.addHead('Updated Types', lvl=1)
576
+ for _type, info in upd_types:
577
+ lines = [f'``{_type}``',
578
+ ]
579
+ for key, valu in sorted(info.items(), key=lambda x: x[0]):
580
+ if key == 'updated_interfaces':
581
+ mesg = f'The type interface has been modified from {valu.get("oldv")}' \
582
+ f' to {valu.get("curv")}.'
583
+ lines.extend(textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
584
+ width=width))
585
+ lines.append('\n')
586
+ elif key == 'updated_opts':
587
+ mesg = f'The type has been modified from {valu.get("oldv")}' \
588
+ f' to {valu.get("curv")}.'
589
+ lines.extend(textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
590
+ width=width))
591
+ lines.append('\n')
592
+ else: # pragma: no cover
593
+ outp.printf(f'Unknown key: {key=} {valu=}')
594
+ raise s_exc.SynErr(mesg=f'Unknown updated type key: {key=} {valu=}')
595
+ rst.addLines(*lines)
596
+
597
+ # Updated Forms
598
+ # We don't really have a "updated forms" to display since the delta for forms data is really property
599
+ # deltas covered elsewhere.
600
+
601
+ # Updated Properties
602
+ upd_props = []
603
+ for form, info in updated_forms.items():
604
+ if 'updated_properties' in info:
605
+ upd_props.append((form, info))
606
+ if upd_props:
607
+ rst.addHead('Updated Properties', lvl=1)
608
+ upd_props.sort(key=lambda x: x[0])
609
+ for form, info in upd_props:
610
+ rst.addLines(f'``{form}``')
611
+ upd_form_props = list(info.get('updated_properties').items())
612
+ if len(upd_form_props) > 1:
613
+ rst.addLines(' The form had the following updated properties:', '\n')
614
+ upd_form_props.sort(key=lambda x: x[0])
615
+ for prop, pnfo in upd_form_props:
616
+ mesg = f'The property ``{prop}`` has been modified from {pnfo.get("old_type")}' \
617
+ f' to {pnfo.get("new_type")}.'
618
+ lines = [
619
+ *textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
620
+ width=width),
621
+ '\n'
622
+ ]
623
+ rst.addLines(*lines)
624
+
625
+ else:
626
+ prop, pnfo = upd_form_props[0]
627
+ mesg = f'The property ``{prop}`` has been modified from {pnfo.get("old_type")}' \
628
+ f' to {pnfo.get("new_type")}.'
629
+ lines = [
630
+ ' The form had the following property updated:',
631
+ '\n',
632
+ *textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ',
633
+ width=width),
634
+ '\n'
635
+ ]
636
+ rst.addLines(*lines)
637
+
638
+ # Light Edges
639
+ if new_edges := changes.get('edges').get('new_edges'):
640
+ new_edges = list(new_edges.items())
641
+ new_edges.sort(key=lambda x: x[0][1])
642
+ rst.addHead('Light Edges', lvl=1)
643
+ for (n1, name, n2), info in new_edges:
644
+ if n1 is not None and n2 is not None:
645
+ mesg = f'''When used with a ``{n1}`` and an ``{n2}`` node, the edge indicates {info.get('doc')}'''
646
+ elif n1 is None and n2 is not None:
647
+ mesg = f'''When used with a ``{n2}`` target node, the edge indicates {info.get('doc')}'''
648
+ elif n1 is not None and n2 is None:
649
+ mesg = f'''When used with a ``{n1}`` node, the edge indicates {info.get('doc')}'''
650
+ else:
651
+ mesg = info.get('doc')
652
+
653
+ rst.addLines(
654
+ f'``{name}``',
655
+ *textwrap.wrap(mesg, initial_indent=' ', subsequent_indent=' ', width=width),
656
+ '\n',
657
+ )
658
+
659
+ # Deprecated Interfaces
660
+ # TODO Support deprecated interfaces!
661
+
662
+ # Deprecated Types
663
+ # Deconflict deprecated forms vs deprecated_types, so we do not
664
+ # not call out types which are also forms in the current model.
665
+ deprecated_types = changes.get('types').get('deprecated_types', {})
666
+ deprecated_forms = {k: v for k, v in deprecated_types.items() if k in current_model.get('forms')}
667
+ deprecated_types = {k: v for k, v in deprecated_types.items() if k not in deprecated_forms}
668
+ if deprecated_types:
669
+ rst.addHead('Deprecated Types', lvl=1)
670
+ rst.addLines('The following types have been marked as deprecated:', '\n')
671
+
672
+ for _type, info in deprecated_types.items():
673
+ rst.addLines(
674
+ f'* ``{_type}``',
675
+ )
676
+ rst.addLines('\n')
677
+
678
+ # Deprecated Forms
679
+ if deprecated_forms:
680
+ rst.addHead('Deprecated Types', lvl=1)
681
+ rst.addLines('The following forms have been marked as deprecated:', '\n')
682
+
683
+ for _type, info in deprecated_forms.items():
684
+ rst.addLines(
685
+ f'* ``{_type}``',
686
+ )
687
+ rst.addLines('\n')
688
+
689
+ # Deprecated Properties
690
+ dep_props = []
691
+ for form, info in updated_forms.items():
692
+ if 'deprecated_properties' in info:
693
+ dep_props.append((form, info))
694
+ if dep_props:
695
+ rst.addHead('Deprecated Properties', lvl=1)
696
+ dep_props.sort(key=lambda x: x[0])
697
+ for form, info in dep_props:
698
+ rst.addLines(f'``{form}``')
699
+ dep_form_props = list(info.get('deprecated_properties').items())
700
+ if len(dep_form_props) > 1:
701
+ rst.addLines(' The form had the following properties deprecated:', '\n')
702
+ dep_form_props.sort(key=lambda x: x[0])
703
+ for name, info in dep_form_props:
704
+ lines = [
705
+ f' ``{name}``',
706
+ *textwrap.wrap(info.get('doc'), initial_indent=' ', subsequent_indent=' ',
707
+ width=width),
708
+ '\n'
709
+ ]
710
+ rst.addLines(*lines)
711
+
712
+ else:
713
+ name, info = dep_form_props[0]
714
+ lines = [
715
+ ' The form had the following property deprecated:',
716
+ '\n'
717
+ f' ``{name}``',
718
+ *textwrap.wrap(info.get('doc'), initial_indent=' ', subsequent_indent=' ',
719
+ width=width),
720
+ '\n'
721
+ ]
722
+ rst.addLines(*lines)
723
+
724
+ return rst
725
+
726
+
727
+ async def format(opts: argparse.Namespace,
74
728
  outp: s_output.OutPut):
75
- if opts.verbose:
76
- outp.printf(f'{opts=}')
77
729
 
78
730
  if not regex.match(version_regex, opts.version):
79
731
  outp.printf(f'Failed to match {opts.version} vs {version_regex}')
@@ -143,16 +795,15 @@ def format(opts: argparse.Namespace,
143
795
  outp.printf(f'No files passed validation from {opts.dir}')
144
796
  return 1
145
797
 
146
- if 'model' in entries:
147
- outp.printf('Model specific entries are not yet implemented.')
148
- return 1
149
-
150
798
  date = opts.date
151
799
  if date is None:
152
800
  date = datetime.datetime.utcnow().strftime('%Y-%m-%d')
153
801
  header = f'{opts.version} - {date}'
154
802
  text = f'{header}\n{"=" * len(header)}\n'
155
803
 
804
+ modeldiff = False
805
+ clean_vers_ref = opts.version.replace(".", "_")
806
+ model_rst_ref = f'userguide_model_{clean_vers_ref}'
156
807
  for key, header in s_schemas._changelogTypes.items():
157
808
  dataz = entries.get(key)
158
809
  if dataz:
@@ -167,8 +818,53 @@ def format(opts: argparse.Namespace,
167
818
  text = f'{text}\n (`#{pr} <https://github.com/vertexproject/synapse/pull/{pr}>`_)'
168
819
  if key == 'migration':
169
820
  text = text + '\n- See :ref:`datamigration` for more information about automatic migrations.'
821
+ elif key == 'model':
822
+ text = text + f'\n- See :ref:`{model_rst_ref}` for more detailed model changes.'
823
+ modeldiff = True
170
824
  text = text + '\n'
171
825
 
826
+ if modeldiff and opts.model_ref:
827
+ # TODO find previous model file automatically?
828
+ if opts.verbose:
829
+ outp.printf(f'Getting reference model from {opts.model_ref}')
830
+
831
+ ref_modl = _getModelFile(opts.model_ref)
832
+
833
+ if opts.model_current:
834
+ to_modl = _getModelFile(opts.model_current)
835
+ cur_modl = to_modl.get('model')
836
+ if opts.verbose:
837
+ outp.printf(f'Comparing {to_modl.get("version")} - {to_modl.get("commit")} vs {ref_modl.get("version")} - {ref_modl.get("commit")}')
838
+ else:
839
+ cur_modl = await _getCurrentModl(outp)
840
+ if opts.verbose:
841
+ outp.printf(f'Comparing current model vs {ref_modl.get("version")} - {ref_modl.get("commit")}')
842
+
843
+ differ = ModelDiffer(cur_modl, ref_modl.get('model'))
844
+ changes = differ.diffModl(outp)
845
+ has_changes = sum([len(v) for v in changes.values()])
846
+ if has_changes:
847
+ rst = _gen_model_rst(opts.version, model_rst_ref, changes, cur_modl, outp, width=opts.width)
848
+ model_text = rst.getRstText()
849
+ if opts.verbose:
850
+ outp.printf(model_text)
851
+ if opts.model_doc_dir:
852
+ fp = s_common.genpath(opts.model_doc_dir, f'model_update_{clean_vers_ref}.rst')
853
+ with s_common.genfile(fp) as fd:
854
+ fd.truncate(0)
855
+ fd.write(model_text.encode())
856
+ outp.printf(f'Wrote model changes to {fp}')
857
+ if opts.verbose:
858
+ outp.printf(f'Adding file to git.')
859
+ argv = ['git', 'add', fp]
860
+ ret = subprocess.run(argv, capture_output=True)
861
+ if opts.verbose:
862
+ outp.printf(f'stddout={ret.stdout}')
863
+ outp.printf(f'stderr={ret.stderr}')
864
+ ret.check_returncode()
865
+ else:
866
+ outp.printf(f'No model changes detected.')
867
+
172
868
  if opts.rm:
173
869
  if opts.verbose:
174
870
  outp.printf('Staging file removals in git')
@@ -180,10 +876,52 @@ def format(opts: argparse.Namespace,
180
876
  outp.printf(f'stderr={ret.stderr}')
181
877
  ret.check_returncode()
182
878
 
879
+ outp.printf('CHANGELOG ENTRY:\n\n')
183
880
  outp.printf(text)
184
881
 
185
882
  return 0
186
883
 
884
+ async def model(opts: argparse.Namespace,
885
+ outp: s_output.OutPut):
886
+
887
+ if opts.save:
888
+ modl = await _getCurrentModl(outp)
889
+
890
+ dirn = s_common.gendir(opts.cdir, 'modelrefs')
891
+ current_commit = _getCurrentCommit(outp)
892
+ if not current_commit:
893
+ return 1
894
+ wrapped_modl = {
895
+ 'model': modl,
896
+ 'commit': current_commit,
897
+ 'version': s_version.version,
898
+ }
899
+
900
+ fp = s_common.genpath(dirn, f'model_{s_version.verstring}_{current_commit}.yaml.gz')
901
+ with s_common.genfile(fp) as fd:
902
+ fd.truncate(0)
903
+ bytz = s_common.yamldump(wrapped_modl)
904
+ small_bytz = gzip.compress(bytz)
905
+ _ = fd.write(small_bytz)
906
+
907
+ outp.printf(f'Saved model to {fp}')
908
+ return 0
909
+
910
+ if opts.compare:
911
+ ref_modl = _getModelFile(opts.compare)
912
+ if opts.to:
913
+ to_modl = _getModelFile(opts.to)
914
+ modl = to_modl.get('model')
915
+ outp.printf(f'Comparing {to_modl.get("version")} - {to_modl.get("commit")} vs {ref_modl.get("version")} - {ref_modl.get("commit")}')
916
+ else:
917
+ modl = await _getCurrentModl(outp)
918
+ outp.printf(f'Comparing current model vs {ref_modl.get("version")} - {ref_modl.get("commit")}')
919
+ differ = ModelDiffer(modl, ref_modl.get('model'))
920
+ changes = differ.diffModl(outp)
921
+ for line in pprint.pformat(changes).splitlines(keepends=False):
922
+ outp.printf(line)
923
+ return 0
924
+
187
925
  async def main(argv, outp=None):
188
926
  if outp is None:
189
927
  outp = s_output.OutPut()
@@ -193,10 +931,14 @@ async def main(argv, outp=None):
193
931
  opts = pars.parse_args(argv)
194
932
  if opts.git_dir_check:
195
933
  if not os.path.exists(os.path.join(os.getcwd(), '.git')):
196
- outp.print('Current working directury must be the root of the repository.')
934
+ outp.printf('Current working directory must be the root of the repository.')
197
935
  return 1
936
+
937
+ if opts.verbose:
938
+ outp.printf(f'{opts=}')
939
+
198
940
  try:
199
- return opts.func(opts, outp)
941
+ return await opts.func(opts, outp)
200
942
  except Exception as e:
201
943
  outp.printf(f'Error running {opts.func}: {traceback.format_exc()}')
202
944
  return 1
@@ -241,8 +983,25 @@ def makeargparser():
241
983
  help='Date to use with the changelog entry')
242
984
  format_pars.add_argument('-r', '--rm', default=False, action='store_true',
243
985
  help='Stage the changelog files as deleted files in git.')
244
-
245
- for p in (gen_pars, format_pars):
986
+ format_pars.add_argument('-m', '--model-ref', default=None, action='store', type=str,
987
+ help='Baseline model to use when generating model deltas. This is normally the previous releases model file.')
988
+ format_pars.add_argument('--model-current', default=None, action='store',
989
+ help='Optional model file to use as a reference as the current model.')
990
+ format_pars.add_argument('--model-doc-dir', default=None, action='store',
991
+ help='Directory to write the model changes too.')
992
+
993
+ model_pars = subpars.add_parser('model', help='Helper for working with the Cortex data model.')
994
+ model_pars.set_defaults(func=model)
995
+ mux_model = model_pars.add_mutually_exclusive_group(required=True)
996
+ mux_model.add_argument('-s', '--save', action='store_true', default=False,
997
+ help='Save a copy of the current model to a file.')
998
+ mux_model.add_argument('-c', '--compare', action='store', default=None,
999
+ help='Model to compare the current model against. Useful for debugging modl diff functionality.'
1000
+ )
1001
+ model_pars.add_argument('-t', '--to', action='store', default=None,
1002
+ help='The model file to compare against. Will not use current model if specified.')
1003
+
1004
+ for p in (gen_pars, format_pars, model_pars):
246
1005
  p.add_argument('-v', '--verbose', default=False, action='store_true',
247
1006
  help='Enable verbose output')
248
1007
  p.add_argument('--cdir', default='./changes', action='store',