synapse 2.175.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 (73) hide show
  1. synapse/axon.py +24 -9
  2. synapse/cortex.py +330 -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/ast.py +6 -5
  8. synapse/lib/auth.py +1520 -0
  9. synapse/lib/cell.py +255 -53
  10. synapse/lib/grammar.py +5 -0
  11. synapse/lib/hive.py +24 -3
  12. synapse/lib/hiveauth.py +6 -32
  13. synapse/lib/layer.py +7 -4
  14. synapse/lib/link.py +21 -17
  15. synapse/lib/lmdbslab.py +149 -0
  16. synapse/lib/modelrev.py +1 -1
  17. synapse/lib/schemas.py +136 -0
  18. synapse/lib/storm.py +70 -33
  19. synapse/lib/stormlib/aha.py +1 -1
  20. synapse/lib/stormlib/auth.py +185 -10
  21. synapse/lib/stormlib/cortex.py +16 -5
  22. synapse/lib/stormlib/gen.py +80 -0
  23. synapse/lib/stormlib/model.py +55 -0
  24. synapse/lib/stormlib/modelext.py +60 -0
  25. synapse/lib/stormlib/storm.py +117 -5
  26. synapse/lib/stormlib/tabular.py +212 -0
  27. synapse/lib/stormtypes.py +14 -1
  28. synapse/lib/trigger.py +1 -1
  29. synapse/lib/version.py +2 -2
  30. synapse/lib/view.py +55 -28
  31. synapse/models/base.py +7 -0
  32. synapse/models/biz.py +4 -0
  33. synapse/models/files.py +8 -1
  34. synapse/models/inet.py +31 -0
  35. synapse/tests/files/changelog/model_2.176.0_16ee721a6b7221344eaf946c3ab4602dda546b1a.yaml.gz +0 -0
  36. synapse/tests/files/changelog/model_2.176.0_2a25c58bbd344716cd7cbc3f4304d8925b0f4ef2.yaml.gz +0 -0
  37. synapse/tests/test_axon.py +7 -4
  38. synapse/tests/test_cortex.py +127 -82
  39. synapse/tests/test_cryotank.py +4 -4
  40. synapse/tests/test_datamodel.py +7 -0
  41. synapse/tests/test_lib_agenda.py +7 -0
  42. synapse/tests/{test_lib_hiveauth.py → test_lib_auth.py} +314 -11
  43. synapse/tests/test_lib_cell.py +161 -8
  44. synapse/tests/test_lib_httpapi.py +18 -14
  45. synapse/tests/test_lib_layer.py +33 -33
  46. synapse/tests/test_lib_link.py +42 -1
  47. synapse/tests/test_lib_lmdbslab.py +68 -0
  48. synapse/tests/test_lib_nexus.py +4 -4
  49. synapse/tests/test_lib_node.py +0 -7
  50. synapse/tests/test_lib_storm.py +45 -0
  51. synapse/tests/test_lib_stormlib_aha.py +1 -2
  52. synapse/tests/test_lib_stormlib_auth.py +21 -0
  53. synapse/tests/test_lib_stormlib_cortex.py +12 -12
  54. synapse/tests/test_lib_stormlib_gen.py +99 -0
  55. synapse/tests/test_lib_stormlib_model.py +108 -0
  56. synapse/tests/test_lib_stormlib_modelext.py +64 -0
  57. synapse/tests/test_lib_stormlib_storm.py +82 -1
  58. synapse/tests/test_lib_stormlib_tabular.py +226 -0
  59. synapse/tests/test_lib_stormsvc.py +4 -1
  60. synapse/tests/test_lib_stormtypes.py +10 -0
  61. synapse/tests/test_model_base.py +3 -0
  62. synapse/tests/test_model_biz.py +3 -0
  63. synapse/tests/test_model_files.py +12 -2
  64. synapse/tests/test_model_inet.py +55 -0
  65. synapse/tests/test_tools_changelog.py +196 -0
  66. synapse/tests/test_tools_healthcheck.py +4 -3
  67. synapse/tests/utils.py +1 -1
  68. synapse/tools/changelog.py +774 -15
  69. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/METADATA +3 -3
  70. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/RECORD +73 -67
  71. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/WHEEL +1 -1
  72. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/LICENSE +0 -0
  73. {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/top_level.txt +0 -0
@@ -919,8 +919,63 @@ class LibModelMigrations(s_stormtypes.Lib, MigrationEditorMixin):
919
919
  'desc': 'Do not copy nodedata to the risk:vulnerable node.'},
920
920
  ),
921
921
  'returns': {'type': 'list', 'desc': 'A list of idens for the risk:vulnerable nodes.'}}},
922
+ {'name': 'inetSslCertToTlsServerCert', 'desc': '''
923
+ Create a inet:tls:servercert node from the provided inet:ssl:cert node.
924
+
925
+ Edits will be made to the inet:tls:servercert node in the current write layer.
926
+
927
+ Tags, tag properties, edges, and node data will be copied
928
+ to the inet:tls:servercert node. However, existing tag properties and
929
+ node data will not be overwritten.
930
+ ''',
931
+ 'type': {'type': 'function', '_funcname': '_storm_query',
932
+ 'args': (
933
+ {'name': 'n', 'type': 'node', 'desc': 'The inet:ssl:cert node to migrate.'},
934
+ {'name': 'nodata', 'type': 'bool', 'default': False,
935
+ 'desc': 'Do not copy nodedata to the inet:tls:servercert node.'},
936
+ ),
937
+ 'returns': {'type': 'node', 'desc': 'The newly created inet:tls:servercert node.'}}},
938
+
922
939
  )
923
940
  _storm_lib_path = ('model', 'migration', 's')
941
+ _storm_query = '''
942
+ function inetSslCertToTlsServerCert(n, nodata=$lib.false) {
943
+ $form = $n.form()
944
+ if ($form != 'inet:ssl:cert') {
945
+ $mesg = `$lib.model.migration.s.inetSslCertToTlsServerCert() only accepts inet:ssl:cert nodes, not {$form}`
946
+ $lib.raise(BadArg, $mesg)
947
+ }
948
+
949
+ $server = $n.props.server
950
+ $sha256 = { yield $n -> file:bytes -> hash:sha256 }
951
+
952
+ if $sha256 {
953
+
954
+ yield $lib.gen.inetTlsServerCertByServerAndSha256($server, $sha256)
955
+
956
+ } else {
957
+
958
+ // File doesn't have a :sha256, try to lift/create a crypto:x509:node based on the file link
959
+ $crypto = { yield $n -> file:bytes -> crypto:x509:cert:file }
960
+ if (not $crypto) {
961
+ $crypto = {[ crypto:x509:cert=($n.props.file,) :file=$n.props.file ]}
962
+ }
963
+
964
+ [ inet:tls:servercert=($server, $crypto) ]
965
+
966
+ }
967
+
968
+ [ .seen ?= $n.props.".seen" ]
969
+
970
+ $lib.model.migration.copyTags($n, $node, overwrite=$lib.false)
971
+ $lib.model.migration.copyEdges($n, $node)
972
+ if (not $nodata) {
973
+ $lib.model.migration.copyData($n, $node, overwrite=$lib.false)
974
+ }
975
+
976
+ return($node)
977
+ }
978
+ '''
924
979
 
925
980
  def getObjLocals(self):
926
981
  return {
@@ -77,6 +77,27 @@ class LibModelExt(s_stormtypes.Lib):
77
77
  {'name': 'model', 'type': 'dict', 'desc': 'A model dictionary from getExtModel().', },
78
78
  ),
79
79
  'returns': {'type': 'boolean'}}},
80
+ {'name': 'addEdge', 'desc': 'Add an extended edge definition to the data model.',
81
+ 'type': {'type': 'function', '_funcname': 'addEdge',
82
+ 'args': (
83
+ {'name': 'n1form', 'type': 'str',
84
+ 'desc': 'The form of the n1 node. May be "*" or null to specify "any".'},
85
+ {'name': 'verb', 'type': 'str', 'desc': 'The edge verb, which must begin with "_".'},
86
+ {'name': 'n2form', 'type': 'str',
87
+ 'desc': 'The form of the n2 node. May be "*" or null to specify "any".'},
88
+ {'name': 'edgeinfo', 'type': 'dict', 'desc': 'A Synapse edge info dictionary.'},
89
+ ),
90
+ 'returns': {'type': 'null'}}},
91
+ {'name': 'delEdge', 'desc': 'Remove an extended edge definition from the data model.',
92
+ 'type': {'type': 'function', '_funcname': 'delEdge',
93
+ 'args': (
94
+ {'name': 'n1form', 'type': 'str',
95
+ 'desc': 'The form of the n1 node. May be "*" or null to specify "any".'},
96
+ {'name': 'verb', 'type': 'str', 'desc': 'The edge verb, which must begin with "_".'},
97
+ {'name': 'n2form', 'type': 'str',
98
+ 'desc': 'The form of the n2 node. May be "*" or null to specify "any".'},
99
+ ),
100
+ 'returns': {'type': 'null'}}},
80
101
  )
81
102
  _storm_lib_path = ('model', 'ext')
82
103
 
@@ -92,6 +113,8 @@ class LibModelExt(s_stormtypes.Lib):
92
113
  'delTagProp': self.delTagProp,
93
114
  'getExtModel': self.getExtModel,
94
115
  'addExtModel': self.addExtModel,
116
+ 'addEdge': self.addEdge,
117
+ 'delEdge': self.delEdge,
95
118
  }
96
119
 
97
120
  # TODO type docs in the new convention
@@ -163,3 +186,40 @@ class LibModelExt(s_stormtypes.Lib):
163
186
  async def addExtModel(self, model):
164
187
  model = await s_stormtypes.toprim(model)
165
188
  return await self.runt.snap.core.addExtModel(model)
189
+
190
+ async def addEdge(self, n1form, verb, n2form, edgeinfo):
191
+ verb = await s_stormtypes.tostr(verb)
192
+ n1form = await s_stormtypes.tostr(n1form, noneok=True)
193
+ n2form = await s_stormtypes.tostr(n2form, noneok=True)
194
+ edgeinfo = await s_stormtypes.toprim(edgeinfo)
195
+
196
+ if not (s_grammar.isEdgeVerb(verb) and verb.islower()):
197
+ mesg = f'Invalid edge verb {verb}'
198
+ raise s_exc.BadEdgeDef(mesg=mesg, n1form=n1form, verb=verb, n2form=n2form)
199
+
200
+ if n1form == '*':
201
+ n1form = None
202
+
203
+ if n2form == '*':
204
+ n2form = None
205
+
206
+ s_stormtypes.confirm(('model', 'edge', 'add'))
207
+ await self.runt.snap.core.addEdge((n1form, verb, n2form), edgeinfo)
208
+
209
+ async def delEdge(self, n1form, verb, n2form):
210
+ verb = await s_stormtypes.tostr(verb)
211
+ n1form = await s_stormtypes.tostr(n1form, noneok=True)
212
+ n2form = await s_stormtypes.tostr(n2form, noneok=True)
213
+
214
+ if not (s_grammar.isEdgeVerb(verb) and verb.islower()):
215
+ mesg = f'Invalid edge verb {verb}'
216
+ raise s_exc.BadEdgeDef(mesg=mesg, n1form=n1form, verb=verb, n2form=n2form)
217
+
218
+ if n1form == '*':
219
+ n1form = None
220
+
221
+ if n2form == '*':
222
+ n2form = None
223
+
224
+ s_stormtypes.confirm(('model', 'edge', 'del'))
225
+ await self.runt.snap.core.delEdge((n1form, verb, n2form))
@@ -1,18 +1,102 @@
1
1
  import logging
2
2
 
3
3
  import synapse.exc as s_exc
4
+ import synapse.common as s_common
4
5
 
6
+ import synapse.lib.storm as s_storm
5
7
  import synapse.lib.stormtypes as s_stormtypes
6
8
 
7
9
  evaldesc = '''\
8
- Evaluate a storm runtime value and optionally cast/coerce it.
10
+ Evaluate a Storm runtime value and optionally cast/coerce it.
9
11
 
10
- NOTE: If storm logging is enabled, the expression being evaluated will be logged
11
- separately.
12
+ Note:
13
+ If Storm logging is enabled, the expression being evaluated will be logged
14
+ separately.
15
+ '''
16
+
17
+ rundesc = '''
18
+ Run a Storm query and yield the messages output by the Storm interpreter.
19
+
20
+ Note:
21
+ If Storm logging is enabled, the query being run will be logged separately.
12
22
  '''
13
23
 
14
24
  stormlogger = logging.getLogger('synapse.storm')
15
25
 
26
+ class StormExecCmd(s_storm.Cmd):
27
+ '''
28
+ Execute text or an embedded query object as Storm in the current pipeline.
29
+
30
+ NOTE: It is recommended to avoid using this where possible to avoid potential
31
+ query injection risks. If you must use this, take care to ensure any values
32
+ being executed have been properly sanitized.
33
+
34
+ Examples:
35
+
36
+ // Add nodes using text in a variable
37
+ $query = '[ inet:fqdn=foo.com inet:fqdn=bar.net ]'
38
+ storm.exec $query
39
+
40
+ // Filter nodes in the pipeline using text in a variable
41
+ $filter = '-:asn=10'
42
+ inet:ipv4:asn
43
+ storm.exec $filter
44
+
45
+ // Pivot using an embedded query
46
+ $pivot = ${ -> inet:asn }
47
+ inet:ipv4:asn
48
+ storm.exec $pivot
49
+ '''
50
+ name = 'storm.exec'
51
+ def getArgParser(self):
52
+ pars = s_storm.Cmd.getArgParser(self)
53
+ pars.add_argument('query', help='The Storm to execute.')
54
+ return pars
55
+
56
+ async def execStormCmd(self, runt, genr):
57
+
58
+ if self.runtsafe:
59
+
60
+ text = await s_stormtypes.tostr(self.opts.query)
61
+ query = await runt.getStormQuery(text)
62
+
63
+ extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
64
+ stormlogger.info(f'Executing storm query via storm.exec {{{text}}} as [{self.runt.user.name}]', extra=extra)
65
+
66
+ async with runt.getSubRuntime(query) as subr:
67
+ async for subp in subr.execute(genr=genr):
68
+ yield subp
69
+
70
+ else:
71
+
72
+ item = None
73
+ async for item in genr:
74
+ break
75
+
76
+ text = await s_stormtypes.tostr(self.opts.query)
77
+ query = await runt.getStormQuery(text)
78
+
79
+ extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
80
+ stormlogger.info(f'Executing storm query via storm.exec {{{text}}} as [{self.runt.user.name}]', extra=extra)
81
+
82
+ async with runt.getSubRuntime(query) as subr:
83
+ async for subp in subr.execute(genr=s_common.agen(item)):
84
+ yield subp
85
+
86
+ async for item in genr:
87
+ text = await s_stormtypes.tostr(self.opts.query)
88
+ query = await runt.getStormQuery(text)
89
+
90
+ subr.runtvars.clear()
91
+ subr.query = query
92
+ subr._initRuntVars(query)
93
+
94
+ extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
95
+ stormlogger.info(f'Executing storm query via storm.exec {{{text}}} as [{self.runt.user.name}]', extra=extra)
96
+
97
+ async for subp in subr.execute(genr=s_common.agen(item)):
98
+ yield subp
99
+
16
100
  @s_stormtypes.registry.registerLib
17
101
  class LibStorm(s_stormtypes.Lib):
18
102
  '''
@@ -25,15 +109,43 @@ class LibStorm(s_stormtypes.Lib):
25
109
  {'name': 'text', 'type': 'str', 'desc': 'A storm expression string.'},
26
110
  {'name': 'cast', 'type': 'str', 'desc': 'A type to cast the result to.', 'default': None},
27
111
  ),
28
- 'returns': {'type': 'any', 'desc': 'The value of the expression and optional cast.', }}},
112
+ 'returns': {'type': 'any', 'desc': 'The value of the expression and optional cast.'}}},
113
+ {'name': 'run', 'desc': rundesc,
114
+ 'type': {'type': 'function', '_funcname': '_runStorm',
115
+ 'args': (
116
+ {'name': 'query', 'type': 'str', 'desc': 'A Storm query string.'},
117
+ {'name': 'opts', 'type': 'dict', 'desc': 'Storm options dictionary.', 'default': None},
118
+ ),
119
+ 'returns': {'name': 'yields', 'type': 'list', 'desc': 'The output messages from the Storm runtime.'}}},
29
120
  )
30
121
  _storm_lib_path = ('storm',)
31
122
 
32
123
  def getObjLocals(self):
33
124
  return {
125
+ 'run': self._runStorm,
34
126
  'eval': self._evalStorm,
35
127
  }
36
128
 
129
+ async def _runStorm(self, query, opts=None):
130
+
131
+ opts = await s_stormtypes.toprim(opts)
132
+ query = await s_stormtypes.tostr(query)
133
+
134
+ if opts is None:
135
+ opts = {}
136
+
137
+ user = opts.get('user')
138
+ if user is None:
139
+ user = opts['user'] = self.runt.user.iden
140
+
141
+ if user != self.runt.user.iden:
142
+ self.runt.confirm(('impersonate',))
143
+
144
+ opts.setdefault('view', self.runt.snap.view.iden)
145
+
146
+ async for mesg in self.runt.snap.view.core.storm(query, opts=opts):
147
+ yield mesg
148
+
37
149
  @s_stormtypes.stormfunc(readonly=True)
38
150
  async def _evalStorm(self, text, cast=None):
39
151
 
@@ -41,7 +153,7 @@ class LibStorm(s_stormtypes.Lib):
41
153
  cast = await s_stormtypes.tostr(cast, noneok=True)
42
154
 
43
155
  if self.runt.snap.core.stormlog:
44
- extra = await self.runt.snap.core.getLogExtra(text=text)
156
+ extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
45
157
  stormlogger.info(f'Executing storm query via $lib.storm.eval() {{{text}}} as [{self.runt.user.name}]', extra=extra)
46
158
 
47
159
  casttype = None
@@ -0,0 +1,212 @@
1
+ import copy
2
+ import textwrap
3
+ import itertools
4
+
5
+ import synapse.exc as s_exc
6
+ import synapse.common as s_common
7
+ import synapse.lib.schemas as s_schemas
8
+ import synapse.lib.stormtypes as s_stormtypes
9
+
10
+ justers = {
11
+ 'left': str.ljust,
12
+ 'center': str.center,
13
+ 'right': str.rjust,
14
+ }
15
+
16
+ @s_stormtypes.registry.registerLib
17
+ class LibTabular(s_stormtypes.Lib):
18
+ '''
19
+ A Storm Library for creating printable tables.
20
+ '''
21
+ _storm_locals = (
22
+ {'name': 'printer', 'desc': '''
23
+ Construct a new printer.
24
+
25
+ Examples:
26
+ Create a simple table using the default separators::
27
+
28
+ $conf = ({
29
+ "columns": [
30
+ {"name": "Year", "width": 4},
31
+ {"name": "Author", "width": 20},
32
+ {"name": "Title", "width": 12},
33
+ ]
34
+ })
35
+
36
+ $printer = $lib.tabular.printer($conf)
37
+
38
+ $lib.print($printer.header())
39
+
40
+ for ($year, $author, $title, $publisher) in $data {
41
+ $lib.print($printer.row(($year, $author, $title))
42
+ }
43
+
44
+ Create a configuration with custom separators and column options::
45
+
46
+ $conf = ({
47
+ "separators": {
48
+ "row:outline": true,
49
+ "column:outline": true,
50
+ "header:row": "#",
51
+ "data:row": "*",
52
+ "column": "+",
53
+ },
54
+ "columns": [
55
+ {"name": "Year", "width": 4, "justify": "right"},
56
+ {"name": "Author", "width": 20, "justify": "center"},
57
+ {"name": "Title", "width": 12, "overflow": "wrap"},
58
+ ]
59
+ })
60
+
61
+ $printer = $lib.tabular.printer($conf)
62
+ ''',
63
+ 'type': {'type': 'function', '_funcname': '_methPrinter',
64
+ 'args': (
65
+ {'name': 'conf', 'type': 'dict',
66
+ 'desc': 'The table configuration dictionary.'},
67
+ ),
68
+ 'returns': {'type': 'tabular:printer',
69
+ 'desc': 'The newly constructed tabular:printer.'}}},
70
+ {'name': 'schema', 'desc': '''
71
+ Get a copy of the table configuration schema.
72
+
73
+ Examples:
74
+ Print a human-readable version of the schema::
75
+
76
+ $schema = $lib.tabular.schema()
77
+ $lib.print($lib.yaml.save($schema))
78
+ ''',
79
+ 'type': {'type': 'function', '_funcname': '_methSchema',
80
+ 'returns': {'type': 'dict',
81
+ 'desc': 'The table configuration schema.'}}},
82
+ )
83
+ _storm_lib_path = ('tabular',)
84
+
85
+ def getObjLocals(self):
86
+ return {
87
+ 'printer': self._methPrinter,
88
+ 'schema': self._methSchema,
89
+ }
90
+
91
+ async def _methSchema(self):
92
+ return copy.deepcopy(s_schemas.tabularConfSchema)
93
+
94
+ async def _methPrinter(self, conf):
95
+ conf = await s_stormtypes.toprim(conf)
96
+ conf.setdefault('separators', {})
97
+ conf = s_schemas.reqValidTabularConf(conf)
98
+ return TabularPrinter(self.runt, conf)
99
+
100
+ @s_stormtypes.registry.registerType
101
+ class TabularPrinter(s_stormtypes.StormType):
102
+ '''
103
+ A Storm object for printing tabular data using a defined configuration.
104
+ '''
105
+ _storm_typename = 'tabular:printer'
106
+ _storm_locals = (
107
+ {'name': 'row', 'desc': 'Create a new row string from a data list.',
108
+ 'type': {'type': 'function', '_funcname': 'row',
109
+ 'args': (
110
+ {'name': 'data', 'type': 'list',
111
+ 'desc': 'The data to create the row from; length must match the number of configured columns.'},
112
+ ),
113
+ 'returns': {'type': 'str', 'desc': 'The row string.'}}},
114
+ {'name': 'header',
115
+ 'desc': 'Create a header row string.',
116
+ 'type': {'type': 'function', '_funcname': 'header',
117
+ 'returns': {'type': 'str', 'desc': 'The header row string.'}}},
118
+ )
119
+
120
+ def __init__(self, runt, conf):
121
+ s_stormtypes.StormType.__init__(self, None)
122
+ self.runt = runt
123
+ self.conf = conf
124
+
125
+ self.firstrow = True
126
+
127
+ self.seprconf = conf['separators']
128
+
129
+ self.colconf = conf['columns']
130
+ self.colcnt = len(self.colconf)
131
+ self.colwidths = [coldef.get('width') or len(coldef['name']) for coldef in self.colconf]
132
+
133
+ self.locls.update({
134
+ 'row': self.row,
135
+ 'header': self.header,
136
+ })
137
+
138
+ def _formatRowLine(self, lineitems, pad=' '):
139
+ sepr = self.seprconf['column']
140
+ endstr = sepr if self.seprconf['column:outline'] else ''
141
+ return f'{endstr}{pad}{f"{pad}{sepr}{pad}".join(lineitems)}{pad}{endstr}'
142
+
143
+ def _makeSeparatorRowStr(self, rowsepr):
144
+ if rowsepr:
145
+ lineitems = (width * rowsepr for width in self.colwidths)
146
+ return self._formatRowLine(lineitems, pad=rowsepr)
147
+ return None
148
+
149
+ def _makeDataRowStrs(self, values):
150
+
151
+ items = []
152
+ for coldef, valu in zip(self.colconf, values):
153
+ width = coldef.get('width')
154
+ valu = str(valu) if valu is not None else ''
155
+
156
+ if width is None:
157
+ if coldef['newlines'] == 'split':
158
+ items.append(valu.split('\n'))
159
+ else:
160
+ items.append([valu.replace('\n', ' ')])
161
+ continue
162
+
163
+ valu = valu.replace('\n', ' ')
164
+
165
+ if len(valu) <= width:
166
+ items.append([justers[coldef['justify']](valu, width)])
167
+ continue
168
+
169
+ if coldef['overflow'] == 'trim':
170
+ items.append([s_common.trimText(valu, n=width)])
171
+ continue
172
+
173
+ items.append([justers[coldef['justify']](item, width) for item in textwrap.wrap(valu, width)])
174
+
175
+ rowstrs = []
176
+ for lineitems in itertools.zip_longest(*items, fillvalue=None):
177
+ lineitems = ([item or self.colwidths[i] * ' ' for i, item in enumerate(lineitems)])
178
+ rowstrs.append(self._formatRowLine(lineitems))
179
+
180
+ return rowstrs
181
+
182
+ async def header(self):
183
+ rowstrs = self._makeDataRowStrs([col['name'] for col in self.colconf])
184
+
185
+ if seprstr := self._makeSeparatorRowStr(self.seprconf['header:row']):
186
+ rowstrs.append(seprstr)
187
+ if self.seprconf['row:outline']:
188
+ rowstrs.insert(0, seprstr)
189
+
190
+ return '\n'.join(rowstrs)
191
+
192
+ async def row(self, data):
193
+ data = await s_stormtypes.toprim(data)
194
+
195
+ if not isinstance(data, (list, tuple)):
196
+ raise s_exc.BadArg(mesg='tabular:printer row() requires a list argument')
197
+
198
+ if len(data) != self.colcnt:
199
+ mesg = 'tabular:printer row() requires data length to equal column count'
200
+ raise s_exc.BadArg(mesg=mesg, length=len(data), column_length=self.colcnt)
201
+
202
+ rowstrs = self._makeDataRowStrs(data)
203
+
204
+ if seprstr := self._makeSeparatorRowStr(self.seprconf['data:row']):
205
+ if self.seprconf['row:outline']:
206
+ rowstrs.append(seprstr)
207
+ elif self.firstrow:
208
+ self.firstrow = False
209
+ else:
210
+ rowstrs.insert(0, seprstr)
211
+
212
+ return '\n'.join(rowstrs)
synapse/lib/stormtypes.py CHANGED
@@ -4138,7 +4138,7 @@ class LibBase64(Lib):
4138
4138
  if urlsafe:
4139
4139
  return base64.urlsafe_b64decode(valu)
4140
4140
  return base64.b64decode(valu)
4141
- except binascii.Error as e:
4141
+ except (binascii.Error, TypeError) as e:
4142
4142
  mesg = f'Error during base64 decoding - {str(e)}: {s_common.trimText(repr(valu))}'
4143
4143
  raise s_exc.StormRuntimeError(mesg=mesg, urlsafe=urlsafe) from None
4144
4144
 
@@ -7532,6 +7532,9 @@ class View(Prim):
7532
7532
  {'name': 'wipeLayer', 'desc': 'Delete all nodes and nodedata from the write layer. Triggers will be run.',
7533
7533
  'type': {'type': 'function', '_funcname': '_methWipeLayer',
7534
7534
  'returns': {'type': 'null', }}},
7535
+ {'name': 'swapLayer', 'desc': 'Swaps the top layer for a fresh one and deletes the old layer.',
7536
+ 'type': {'type': 'function', '_funcname': '_methSwapLayer',
7537
+ 'returns': {'type': 'null', }}},
7535
7538
  {'name': 'addNode', 'desc': '''Transactionally add a single node and all it's properties. If any validation fails, no changes are made.''',
7536
7539
  'type': {'type': 'function', '_funcname': 'addNode',
7537
7540
  'args': (
@@ -7717,6 +7720,7 @@ class View(Prim):
7717
7720
  'addNode': self.addNode,
7718
7721
  'getEdges': self._methGetEdges,
7719
7722
  'wipeLayer': self._methWipeLayer,
7723
+ 'swapLayer': self._methSwapLayer,
7720
7724
  'addNodeEdits': self._methAddNodeEdits,
7721
7725
  'getEdgeVerbs': self._methGetEdgeVerbs,
7722
7726
  'getFormCounts': self._methGetFormcount,
@@ -8017,6 +8021,15 @@ class View(Prim):
8017
8021
  view = self.runt.snap.core.getView(viewiden)
8018
8022
  await view.wipeLayer(useriden=useriden)
8019
8023
 
8024
+ async def _methSwapLayer(self):
8025
+
8026
+ view = self._reqView()
8027
+
8028
+ self.runt.reqAdmin(gateiden=view.iden)
8029
+ self.runt.confirm(('layer', 'del'), gateiden=view.layers[0].iden)
8030
+
8031
+ await view.swapLayer()
8032
+
8020
8033
  async def getMerges(self):
8021
8034
  view = self._reqView()
8022
8035
  async for merge in view.getMerges():
synapse/lib/trigger.py CHANGED
@@ -494,7 +494,7 @@ class Trigger:
494
494
  await self.view.core.getStormQuery(valu)
495
495
 
496
496
  self.tdef[name] = valu
497
- await self.view.trigdict.set(self.iden, self.tdef)
497
+ self.view.trigdict.set(self.iden, self.tdef)
498
498
 
499
499
  def get(self, name):
500
500
  return self.tdef.get(name)
synapse/lib/version.py CHANGED
@@ -223,6 +223,6 @@ def reqVersion(valu, reqver,
223
223
  ##############################################################################
224
224
  # The following are touched during the release process by bumpversion.
225
225
  # Do not modify these directly.
226
- version = (2, 175, 0)
226
+ version = (2, 177, 0)
227
227
  verstring = '.'.join([str(x) for x in version])
228
- commit = 'c331c50ef1e7b2234be997c4b791fb8b8bed7cf2'
228
+ commit = 'b7da795aeb690020e38e99dc5aa1f505cdc2d659'