synapse 2.195.1__py311-none-any.whl → 2.196.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 (45) hide show
  1. synapse/axon.py +72 -5
  2. synapse/common.py +23 -0
  3. synapse/cortex.py +1 -0
  4. synapse/daemon.py +1 -0
  5. synapse/lib/aha.py +159 -4
  6. synapse/lib/cell.py +133 -8
  7. synapse/lib/jsonstor.py +2 -1
  8. synapse/lib/modelrev.py +5 -1
  9. synapse/lib/nexus.py +4 -1
  10. synapse/lib/reflect.py +4 -5
  11. synapse/lib/snap.py +14 -7
  12. synapse/lib/stormlib/aha.py +351 -1
  13. synapse/lib/stormlib/utils.py +37 -0
  14. synapse/lib/stormtypes.py +118 -0
  15. synapse/lib/version.py +2 -2
  16. synapse/models/base.py +3 -0
  17. synapse/models/infotech.py +55 -16
  18. synapse/models/orgs.py +14 -10
  19. synapse/models/risk.py +23 -10
  20. synapse/models/transport.py +8 -3
  21. synapse/telepath.py +12 -0
  22. synapse/tests/test_axon.py +23 -0
  23. synapse/tests/test_common.py +28 -0
  24. synapse/tests/test_datamodel.py +8 -0
  25. synapse/tests/test_lib_aha.py +241 -0
  26. synapse/tests/test_lib_cell.py +61 -0
  27. synapse/tests/test_lib_jsonstor.py +1 -0
  28. synapse/tests/test_lib_modelrev.py +6 -0
  29. synapse/tests/test_lib_stormlib_aha.py +188 -0
  30. synapse/tests/test_lib_stormlib_utils.py +14 -0
  31. synapse/tests/test_lib_stormtypes.py +90 -3
  32. synapse/tests/test_model_base.py +2 -0
  33. synapse/tests/test_model_infotech.py +28 -1
  34. synapse/tests/test_model_orgs.py +2 -0
  35. synapse/tests/test_model_risk.py +2 -0
  36. synapse/tests/test_model_transport.py +1 -0
  37. synapse/tests/test_telepath.py +26 -0
  38. synapse/tests/test_tools_aha.py +192 -0
  39. synapse/tools/aha/mirror.py +193 -0
  40. synapse/tools/changelog.py +32 -27
  41. {synapse-2.195.1.dist-info → synapse-2.196.0.dist-info}/METADATA +1 -1
  42. {synapse-2.195.1.dist-info → synapse-2.196.0.dist-info}/RECORD +45 -42
  43. {synapse-2.195.1.dist-info → synapse-2.196.0.dist-info}/LICENSE +0 -0
  44. {synapse-2.195.1.dist-info → synapse-2.196.0.dist-info}/WHEEL +0 -0
  45. {synapse-2.195.1.dist-info → synapse-2.196.0.dist-info}/top_level.txt +0 -0
@@ -3,14 +3,17 @@ import shutil
3
3
 
4
4
  from unittest import mock
5
5
 
6
+ import synapse.exc as s_exc
6
7
  import synapse.common as s_common
7
8
  import synapse.lib.cell as s_cell
9
+ import synapse.lib.version as s_version
8
10
 
9
11
  import synapse.tests.utils as s_t_utils
10
12
 
11
13
  import synapse.tools.aha.list as s_a_list
12
14
  import synapse.tools.aha.clone as s_a_clone
13
15
  import synapse.tools.aha.enroll as s_a_enroll
16
+ import synapse.tools.aha.mirror as s_a_mirror
14
17
  import synapse.tools.aha.easycert as s_a_easycert
15
18
  import synapse.tools.aha.provision.user as s_a_provision_user
16
19
 
@@ -170,3 +173,192 @@ class AhaToolsTest(s_t_utils.SynTest):
170
173
 
171
174
  teleyaml = s_common.yamlload(syndir, 'telepath.yaml')
172
175
  self.eq(teleyaml.get('version'), 1)
176
+
177
+ async def test_aha_mirror(self):
178
+
179
+ async with self.getTestAha() as aha:
180
+
181
+ base_svcinfo = {
182
+ 'iden': 'test_iden',
183
+ 'leader': 'leader',
184
+ 'urlinfo': {
185
+ 'scheme': 'tcp',
186
+ 'host': '127.0.0.1',
187
+ 'port': 0,
188
+ 'hostname': 'test.host'
189
+ }
190
+ }
191
+
192
+ conf_no_iden = {'aha:provision': await aha.addAhaSvcProv('no.iden')}
193
+ async with self.getTestCell(s_cell.Cell, conf=conf_no_iden) as cell_no_iden:
194
+ svcinfo = {k: v for k, v in base_svcinfo.items() if k != 'iden'}
195
+ await aha.addAhaSvc('no.iden', svcinfo)
196
+
197
+ argv = ['--url', aha.getLocalUrl()]
198
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
199
+ self.eq(retn, 0)
200
+ outp.expect('Service Mirror Groups:')
201
+ self.notin('no.iden', str(outp))
202
+
203
+ conf_no_host = {'aha:provision': await aha.addAhaSvcProv('no.host')}
204
+ async with self.getTestCell(s_cell.Cell, conf=conf_no_host) as cell_no_host:
205
+ svcinfo = dict(base_svcinfo)
206
+ svcinfo['urlinfo'] = {k: v for k, v in base_svcinfo['urlinfo'].items() if k != 'hostname'}
207
+ await aha.addAhaSvc('no.host', svcinfo)
208
+
209
+ argv = ['--url', aha.getLocalUrl()]
210
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
211
+ self.eq(retn, 0)
212
+ outp.expect('Service Mirror Groups:')
213
+ self.notin('no.host', str(outp))
214
+
215
+ conf_no_leader = {'aha:provision': await aha.addAhaSvcProv('no.leader')}
216
+ async with self.getTestCell(s_cell.Cell, conf=conf_no_leader) as cell_no_leader:
217
+ svcinfo = {k: v for k, v in base_svcinfo.items() if k != 'leader'}
218
+ await aha.addAhaSvc('no.leader', svcinfo)
219
+
220
+ argv = ['--url', aha.getLocalUrl()]
221
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
222
+ self.eq(retn, 0)
223
+ outp.expect('Service Mirror Groups:')
224
+ self.notin('no.leader', str(outp))
225
+
226
+ conf_no_primary = {'aha:provision': await aha.addAhaSvcProv('no.primary')}
227
+ async with self.getTestCell(s_cell.Cell, conf=conf_no_primary) as cell_no_primary:
228
+ svcinfo = dict(base_svcinfo)
229
+ svcinfo['urlinfo']['hostname'] = 'nonexistent.host'
230
+ await aha.addAhaSvc('no.primary', svcinfo)
231
+
232
+ async with aha.waiter(3, 'aha:svcadd', timeout=10):
233
+
234
+ conf = {'aha:provision': await aha.addAhaSvcProv('00.cell')}
235
+ cell00 = await aha.enter_context(self.getTestCell(conf=conf))
236
+
237
+ conf = {'aha:provision': await aha.addAhaSvcProv('01.cell', {'mirror': 'cell'})}
238
+ cell01 = await aha.enter_context(self.getTestCell(conf=conf))
239
+
240
+ await cell01.sync()
241
+
242
+ ahaurl = aha.getLocalUrl()
243
+
244
+ argv = ['--url', ahaurl]
245
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
246
+ self.eq(retn, 0)
247
+ outp.expect('Service Mirror Groups:')
248
+ outp.expect('00.cell.synapse')
249
+ outp.expect('01.cell.synapse')
250
+ outp.expect('Group Status: In Sync')
251
+
252
+ argv = ['--url', ahaurl, '--timeout', '30']
253
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
254
+ self.eq(retn, 0)
255
+
256
+ with mock.patch('synapse.telepath.Proxy._hasTeleFeat',
257
+ return_value=False):
258
+ argv = ['--url', ahaurl]
259
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
260
+ self.eq(retn, 1)
261
+ outp.expect(f'Service at {ahaurl} does not support the required callpeers feature.')
262
+
263
+ with mock.patch('synapse.telepath.Proxy._hasTeleFeat',
264
+ side_effect=s_exc.NoSuchMeth(name='_hasTeleFeat')):
265
+ argv = ['--url', ahaurl]
266
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
267
+ self.eq(retn, 1)
268
+ outp.expect(f'Service at {ahaurl} does not support the required callpeers feature.')
269
+
270
+ argv = ['--url', 'tcp://newp:1234/']
271
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
272
+ self.eq(retn, 1)
273
+ outp.expect('ERROR:')
274
+
275
+ async def mockCellInfo():
276
+ return {
277
+ 'cell': {'ready': True, 'nexsindx': 10, 'uplink': None},
278
+ 'synapse': {'verstring': s_version.verstring},
279
+ }
280
+
281
+ async def mockOutOfSyncCellInfo():
282
+ return {
283
+ 'cell': {'ready': True, 'nexsindx': 5, 'uplink': cell00.iden},
284
+ 'synapse': {'verstring': s_version.verstring},
285
+ }
286
+
287
+ with mock.patch.object(cell00, 'getCellInfo', mockCellInfo):
288
+ with mock.patch.object(cell01, 'getCellInfo', mockOutOfSyncCellInfo):
289
+ async def mock_call_aha(*args, **kwargs):
290
+ todo = args[1]
291
+ if todo[0] == 'waitNexsOffs':
292
+ yield ('00.cell.synapse', (True, True))
293
+ yield ('01.cell.synapse', (True, True))
294
+ elif todo[0] == 'getCellInfo':
295
+ if not hasattr(mock_call_aha, 'called'):
296
+ mock_call_aha.called = True
297
+ yield ('00.cell.synapse', (True, await mockCellInfo()))
298
+ yield ('01.cell.synapse', (True, await mockOutOfSyncCellInfo()))
299
+ else:
300
+ yield ('00.cell.synapse', (True, await mockCellInfo()))
301
+ yield ('01.cell.synapse', (True, await mockCellInfo()))
302
+
303
+ with mock.patch.object(aha, 'callAhaPeerApi', mock_call_aha):
304
+ argv = ['--url', ahaurl, '--wait']
305
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
306
+ self.eq(retn, 0)
307
+ outp.expect('Group Status: Out of Sync')
308
+ outp.expect('Updated status:')
309
+ outp.expect('Group Status: In Sync')
310
+
311
+ with mock.patch.object(cell00, 'getCellInfo', mockCellInfo):
312
+ with mock.patch.object(cell01, 'getCellInfo', mockOutOfSyncCellInfo):
313
+ argv = ['--url', ahaurl, '--timeout', '1']
314
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
315
+ self.eq(retn, 0)
316
+ outp.expect('Group Status: Out of Sync')
317
+
318
+ async with self.getTestCore() as core:
319
+ curl = core.getLocalUrl()
320
+ argv = ['--url', curl]
321
+ with mock.patch('synapse.telepath.Proxy._hasTeleFeat',
322
+ return_value=True):
323
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
324
+ self.eq(1, retn)
325
+ outp.expect(f'Service at {curl} is not an Aha server')
326
+
327
+ async with aha.waiter(1, 'aha:svcadd', timeout=10):
328
+
329
+ conf = {'aha:provision': await aha.addAhaSvcProv('02.cell', {'mirror': 'cell'})}
330
+ cell02 = await aha.enter_context(self.getTestCell(conf=conf))
331
+ await cell02.sync()
332
+
333
+ async def mock_failed_api(*args, **kwargs):
334
+ yield ('00.cell.synapse', (True, {'cell': {'ready': True, 'nexsindx': 10}}))
335
+ yield ('01.cell.synapse', (False, 'error'))
336
+ yield ('02.cell.synapse', (True, {'cell': {'ready': True, 'nexsindx': 12}}))
337
+
338
+ with mock.patch.object(aha, 'callAhaPeerApi', mock_failed_api):
339
+ argv = ['--url', ahaurl, '--timeout', '1']
340
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
341
+ outp.expect('00.cell.synapse leader True True 127.0.0.1', whitespace=False)
342
+ outp.expect('nexsindx 10', whitespace=False)
343
+ outp.expect('02.cell.synapse leader True True 127.0.0.1', whitespace=False)
344
+ outp.expect('nexsindx 12', whitespace=False)
345
+ outp.expect('01.cell.synapse <unknown> True True', whitespace=False)
346
+ outp.expect('<unknown> <unknown>', whitespace=False)
347
+
348
+ self.eq(s_a_mirror.timeout_type('30'), 30)
349
+ self.eq(s_a_mirror.timeout_type('0'), 0)
350
+
351
+ with self.raises(s_exc.BadArg) as cm:
352
+ s_a_mirror.timeout_type('-1')
353
+ self.isin('is not a valid non-negative integer', cm.exception.get('mesg'))
354
+
355
+ with self.raises(s_exc.BadArg) as cm:
356
+ s_a_mirror.timeout_type('foo')
357
+ self.isin('is not a valid non-negative integer', cm.exception.get('mesg'))
358
+
359
+ synerr = s_exc.SynErr(mesg='Oof')
360
+ with mock.patch('synapse.telepath.openurl', side_effect=synerr):
361
+ argv = ['--url', 'tcp://test:1234/']
362
+ retn, outp = await self.execToolMain(s_a_mirror.main, argv)
363
+ self.eq(retn, 1)
364
+ outp.expect('ERROR: Oof')
@@ -0,0 +1,193 @@
1
+ import sys
2
+ import asyncio
3
+ import argparse
4
+
5
+ import synapse.exc as s_exc
6
+ import synapse.common as s_common
7
+ import synapse.telepath as s_telepath
8
+
9
+ import synapse.lib.output as s_output
10
+ import synapse.lib.version as s_version
11
+
12
+ descr = '''
13
+ Query the Aha server for the service cluster status of mirrors.
14
+
15
+ Examples:
16
+
17
+ python -m synapse.tools.aha.mirror --timeout 30
18
+
19
+ '''
20
+
21
+ async def get_cell_infos(prox, iden, members, timeout):
22
+ cell_infos = {}
23
+ if iden is not None:
24
+ todo = s_common.todo('getCellInfo')
25
+ async for svcname, (ok, info) in prox.callAhaPeerApi(iden, todo, timeout=timeout):
26
+ if not ok:
27
+ continue
28
+ cell_infos[svcname] = info
29
+ return cell_infos
30
+
31
+ def build_status_list(members, cell_infos):
32
+ group_status = []
33
+ for svc in members:
34
+ svcname = svc.get('name')
35
+ svcinfo = svc.get('svcinfo', {})
36
+ status = {
37
+ 'name': svcname,
38
+ 'role': '<unknown>',
39
+ 'online': str('online' in svcinfo),
40
+ 'ready': 'True',
41
+ 'host': svcinfo.get('urlinfo', {}).get('host', ''),
42
+ 'port': str(svcinfo.get('urlinfo', {}).get('port', '')),
43
+ 'version': '<unknown>',
44
+ 'nexs_indx': 0
45
+ }
46
+ if svcname in cell_infos:
47
+ info = cell_infos[svcname]
48
+ cell_info = info.get('cell', {})
49
+ status.update({
50
+ 'nexs_indx': cell_info.get('nexsindx', 0),
51
+ 'role': 'follower' if cell_info.get('uplink') else 'leader',
52
+ 'version': str(info.get('synapse', {}).get('verstring', '')),
53
+ 'online': 'True',
54
+ 'ready': str(cell_info.get('ready', False))
55
+ })
56
+ group_status.append(status)
57
+ return group_status
58
+
59
+ def output_status(outp, vname, group_status):
60
+ header = ' {:<40} {:<10} {:<8} {:<7} {:<16} {:<9} {:<12} {:<10}'.format(
61
+ 'name', 'role', 'online', 'ready', 'host', 'port', 'version', 'nexus idx')
62
+ outp.printf(header)
63
+ outp.printf('#' * 120)
64
+ outp.printf(vname)
65
+ for status in group_status:
66
+ if status['nexs_indx'] == 0:
67
+ status['nexs_indx'] = '<unknown>'
68
+ line = ' {name:<40} {role:<10} {online:<8} {ready:<7} {host:<16} {port:<9} {version:<12} {nexs_indx:<10}'.format(**status)
69
+ outp.printf(line)
70
+
71
+ def check_sync_status(group_status):
72
+ indices = {status['nexs_indx'] for status in group_status}
73
+ known_count = sum(1 for status in group_status)
74
+ return len(indices) == 1 and known_count == len(group_status)
75
+
76
+ def timeout_type(valu):
77
+ try:
78
+ ivalu = int(valu)
79
+ if ivalu < 0:
80
+ raise ValueError
81
+ except ValueError:
82
+ raise s_exc.BadArg(mesg=f"{valu} is not a valid non-negative integer")
83
+ return ivalu
84
+
85
+ async def main(argv, outp=s_output.stdout):
86
+
87
+ pars = argparse.ArgumentParser(prog='synapse.tools.aha.mirror', description=descr,
88
+ formatter_class=argparse.RawDescriptionHelpFormatter)
89
+
90
+ pars.add_argument('--url', default='cell:///vertex/storage', help='The telepath URL to connect to the AHA service.')
91
+ pars.add_argument('--timeout', type=timeout_type, default=10, help='The timeout in seconds for individual service API calls')
92
+ pars.add_argument('--wait', action='store_true', help='Whether to wait for the mirrors to sync.')
93
+ opts = pars.parse_args(argv)
94
+
95
+ async with s_telepath.withTeleEnv():
96
+ try:
97
+ async with await s_telepath.openurl(opts.url) as prox:
98
+ try:
99
+ if not prox._hasTeleFeat('callpeers', vers=1):
100
+ outp.printf(f'Service at {opts.url} does not support the required callpeers feature.')
101
+ return 1
102
+ except s_exc.NoSuchMeth:
103
+ outp.printf(f'Service at {opts.url} does not support the required callpeers feature.')
104
+ return 1
105
+ classes = prox._getClasses()
106
+ if 'synapse.lib.aha.AhaApi' not in classes:
107
+ outp.printf(f'Service at {opts.url} is not an Aha server')
108
+ return 1
109
+
110
+ virtual_services, member_servers = {}, {}
111
+ async for svc in prox.getAhaSvcs():
112
+ name = svc.get('name', '')
113
+ svcinfo = svc.get('svcinfo', {})
114
+ urlinfo = svcinfo.get('urlinfo', {})
115
+ hostname = urlinfo.get('hostname', '')
116
+
117
+ if name != hostname:
118
+ virtual_services[name] = svc
119
+ else:
120
+ member_servers[name] = svc
121
+
122
+ mirror_groups = {}
123
+ for vname, vsvc in virtual_services.items():
124
+ vsvc_info = vsvc.get('svcinfo', {})
125
+ vsvc_iden = vsvc_info.get('iden')
126
+ vsvc_leader = vsvc_info.get('leader')
127
+ vsvc_hostname = vsvc_info.get('urlinfo', {}).get('hostname', '')
128
+
129
+ if not vsvc_iden or not vsvc_hostname or not vsvc_leader:
130
+ continue
131
+
132
+ primary_member = member_servers.get(vsvc_hostname)
133
+ if not primary_member:
134
+ continue
135
+
136
+ members = [primary_member] + [
137
+ msvc for mname, msvc in member_servers.items()
138
+ if mname != vsvc_hostname and
139
+ msvc.get('svcinfo', {}).get('iden') == vsvc_iden and
140
+ msvc.get('svcinfo', {}).get('leader') == vsvc_leader
141
+ ]
142
+
143
+ if len(members) > 1:
144
+ mirror_groups[vname] = members
145
+
146
+ outp.printf('Service Mirror Groups:')
147
+ for vname, members in mirror_groups.items():
148
+ iden = members[0].get('svcinfo', {}).get('iden')
149
+
150
+ cell_infos = await get_cell_infos(prox, iden, members, opts.timeout)
151
+ group_status = build_status_list(members, cell_infos)
152
+ output_status(outp, vname, group_status)
153
+
154
+ if check_sync_status(group_status):
155
+ outp.printf('Group Status: In Sync')
156
+ else:
157
+ outp.printf(f'Group Status: Out of Sync')
158
+ if opts.wait:
159
+ leader_nexs = None
160
+ for status in group_status:
161
+ if status['role'] == 'leader' and isinstance(status['nexs_indx'], int):
162
+ leader_nexs = status['nexs_indx']
163
+
164
+ if leader_nexs is not None:
165
+ while True:
166
+ responses = []
167
+ todo = s_common.todo('waitNexsOffs', leader_nexs - 1, timeout=opts.timeout)
168
+ async for svcname, (ok, info) in prox.callAhaPeerApi(iden, todo, timeout=opts.timeout):
169
+ if ok and info:
170
+ responses.append((svcname, info))
171
+
172
+ if len(responses) == len(members):
173
+ cell_infos = await get_cell_infos(prox, iden, members, opts.timeout)
174
+ group_status = build_status_list(members, cell_infos)
175
+
176
+ outp.printf('\nUpdated status:')
177
+ output_status(outp, vname, group_status)
178
+
179
+ if check_sync_status(group_status):
180
+ outp.printf('Group Status: In Sync')
181
+ break
182
+
183
+ return 0
184
+
185
+ except Exception as e:
186
+ mesg = repr(e)
187
+ if isinstance(e, s_exc.SynErr):
188
+ mesg = e.errinfo.get('mesg', repr(e))
189
+ outp.printf(f'ERROR: {mesg}')
190
+ return 1
191
+
192
+ if __name__ == '__main__': # pragma: no cover
193
+ sys.exit(asyncio.run(main(sys.argv[1:])))
@@ -886,34 +886,8 @@ async def format(opts: argparse.Namespace,
886
886
  modeldiff = False
887
887
  clean_vers_ref = opts.version.replace(".", "_")
888
888
  model_rst_ref = f'userguide_model_{clean_vers_ref}'
889
- for key, header in s_schemas._changelogTypes.items():
890
- dataz = entries.get(key)
891
- if dataz:
892
- text = text + f'\n{header}\n{"-" * len(header)}'
893
- dataz.sort(key=lambda x: x.get('prs'))
894
- for data in dataz:
895
- desc = data.get('desc') # type: str
896
- desc_lines = desc.splitlines()
897
- for i, chunk in enumerate(desc_lines):
898
- if i == 0:
899
- for line in textwrap.wrap(chunk, initial_indent='- ', subsequent_indent=' ', width=opts.width):
900
- text = f'{text}\n{line}'
901
- else:
902
- text = text + '\n'
903
- for line in textwrap.wrap(chunk, initial_indent=' ', subsequent_indent=' ', width=opts.width):
904
- text = f'{text}\n{line}'
905
-
906
- if not opts.hide_prs:
907
- for pr in data.get('prs'):
908
- text = f'{text}\n (`#{pr} <https://github.com/vertexproject/synapse/pull/{pr}>`_)'
909
- if key == 'migration':
910
- text = text + '\n- See :ref:`datamigration` for more information about automatic migrations.'
911
- elif key == 'model':
912
- text = text + f'\n- See :ref:`{model_rst_ref}` for more detailed model changes.'
913
- modeldiff = True
914
- text = text + '\n'
915
889
 
916
- if modeldiff and opts.model_ref:
890
+ if opts.model_ref:
917
891
  # TODO find previous model file automatically?
918
892
  if opts.verbose:
919
893
  outp.printf(f'Getting reference model from {opts.model_ref}')
@@ -934,6 +908,8 @@ async def format(opts: argparse.Namespace,
934
908
  changes = differ.diffModl(outp)
935
909
  has_changes = sum([len(v) for v in changes.values()])
936
910
  if has_changes:
911
+ entries['model'].append({'prs': [], 'type': 'skip'})
912
+ modeldiff = True
937
913
  rst = _gen_model_rst(opts.version, model_rst_ref, changes, cur_modl, outp, width=opts.width)
938
914
  model_text = rst.getRstText()
939
915
  if opts.verbose:
@@ -955,6 +931,35 @@ async def format(opts: argparse.Namespace,
955
931
  else:
956
932
  outp.printf(f'No model changes detected.')
957
933
 
934
+ for key, header in s_schemas._changelogTypes.items():
935
+ dataz = entries.get(key)
936
+ if dataz:
937
+ text = text + f'\n{header}\n{"-" * len(header)}'
938
+ dataz.sort(key=lambda x: x.get('prs'))
939
+ for data in dataz:
940
+ desc = data.get('desc') # type: str
941
+ if desc is None and data.get('type') == 'skip':
942
+ continue
943
+ desc_lines = desc.splitlines()
944
+ for i, chunk in enumerate(desc_lines):
945
+ if i == 0:
946
+ for line in textwrap.wrap(chunk, initial_indent='- ', subsequent_indent=' ', width=opts.width):
947
+ text = f'{text}\n{line}'
948
+ else:
949
+ text = text + '\n'
950
+ for line in textwrap.wrap(chunk, initial_indent=' ', subsequent_indent=' ', width=opts.width):
951
+ text = f'{text}\n{line}'
952
+
953
+ if not opts.hide_prs:
954
+ for pr in data.get('prs'):
955
+ text = f'{text}\n (`#{pr} <https://github.com/vertexproject/synapse/pull/{pr}>`_)'
956
+ if key == 'migration':
957
+ text = text + '\n- See :ref:`datamigration` for more information about automatic migrations.'
958
+ elif key == 'model':
959
+ if modeldiff:
960
+ text = text + f'\n- See :ref:`{model_rst_ref}` for more detailed model changes.'
961
+ text = text + '\n'
962
+
958
963
  if opts.rm:
959
964
  if opts.verbose:
960
965
  outp.printf('Staging file removals in git')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: synapse
3
- Version: 2.195.1
3
+ Version: 2.196.0
4
4
  Summary: Synapse Intelligence Analysis Framework
5
5
  Author-email: The Vertex Project LLC <root@vertex.link>
6
6
  License: Apache License 2.0