synapse 2.180.1__py311-none-any.whl → 2.181.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 (85) hide show
  1. synapse/assets/__init__.py +35 -0
  2. synapse/assets/storm/migrations/model-0.2.28.storm +355 -0
  3. synapse/common.py +2 -1
  4. synapse/cortex.py +49 -35
  5. synapse/cryotank.py +1 -1
  6. synapse/datamodel.py +30 -0
  7. synapse/lib/ast.py +12 -7
  8. synapse/lib/cell.py +1 -1
  9. synapse/lib/chop.py +0 -1
  10. synapse/lib/drive.py +8 -8
  11. synapse/lib/layer.py +55 -13
  12. synapse/lib/lmdbslab.py +26 -5
  13. synapse/lib/modelrev.py +28 -1
  14. synapse/lib/modules.py +1 -0
  15. synapse/lib/nexus.py +1 -1
  16. synapse/lib/node.py +5 -0
  17. synapse/lib/parser.py +23 -16
  18. synapse/lib/scrape.py +1 -1
  19. synapse/lib/slabseqn.py +2 -2
  20. synapse/lib/snap.py +129 -0
  21. synapse/lib/storm.lark +16 -2
  22. synapse/lib/storm.py +3 -0
  23. synapse/lib/storm_format.py +1 -0
  24. synapse/lib/stormhttp.py +34 -1
  25. synapse/lib/stormlib/auth.py +1 -1
  26. synapse/lib/stormlib/cortex.py +5 -2
  27. synapse/lib/stormlib/ipv6.py +2 -2
  28. synapse/lib/stormlib/model.py +114 -12
  29. synapse/lib/stormlib/project.py +1 -1
  30. synapse/lib/stormtypes.py +81 -7
  31. synapse/lib/types.py +7 -0
  32. synapse/lib/version.py +2 -2
  33. synapse/lib/view.py +47 -0
  34. synapse/models/inet.py +10 -3
  35. synapse/models/infotech.py +2 -1
  36. synapse/models/language.py +4 -0
  37. synapse/models/math.py +50 -0
  38. synapse/models/orgs.py +8 -0
  39. synapse/models/risk.py +9 -0
  40. synapse/tests/files/stormcov/pragma-nocov.storm +18 -0
  41. synapse/tests/test_assets.py +25 -0
  42. synapse/tests/test_cortex.py +129 -0
  43. synapse/tests/test_datamodel.py +6 -0
  44. synapse/tests/test_lib_grammar.py +7 -1
  45. synapse/tests/test_lib_layer.py +35 -0
  46. synapse/tests/test_lib_lmdbslab.py +11 -9
  47. synapse/tests/test_lib_modelrev.py +655 -1
  48. synapse/tests/test_lib_slabseqn.py +5 -4
  49. synapse/tests/test_lib_snap.py +4 -0
  50. synapse/tests/test_lib_storm.py +72 -1
  51. synapse/tests/test_lib_stormhttp.py +99 -1
  52. synapse/tests/test_lib_stormlib_cortex.py +21 -4
  53. synapse/tests/test_lib_stormlib_iters.py +8 -5
  54. synapse/tests/test_lib_stormlib_model.py +45 -6
  55. synapse/tests/test_lib_stormtypes.py +158 -2
  56. synapse/tests/test_lib_types.py +6 -0
  57. synapse/tests/test_model_inet.py +10 -0
  58. synapse/tests/test_model_language.py +4 -0
  59. synapse/tests/test_model_math.py +22 -0
  60. synapse/tests/test_model_orgs.py +6 -2
  61. synapse/tests/test_model_risk.py +4 -0
  62. synapse/tests/test_utils_stormcov.py +5 -0
  63. synapse/tests/utils.py +18 -5
  64. synapse/utils/stormcov/plugin.py +31 -1
  65. synapse/vendor/cpython/LICENSE +279 -0
  66. synapse/vendor/cpython/__init__.py +0 -0
  67. synapse/vendor/cpython/lib/__init__.py +0 -0
  68. synapse/vendor/cpython/lib/email/__init__.py +0 -0
  69. synapse/vendor/cpython/lib/email/_parseaddr.py +560 -0
  70. synapse/vendor/cpython/lib/email/utils.py +505 -0
  71. synapse/vendor/cpython/lib/ipaddress.py +2366 -0
  72. synapse/vendor/cpython/lib/test/__init__.py +0 -0
  73. synapse/vendor/cpython/lib/test/support/__init__.py +114 -0
  74. synapse/vendor/cpython/lib/test/test_email/__init__.py +0 -0
  75. synapse/vendor/cpython/lib/test/test_email/test_email.py +480 -0
  76. synapse/vendor/cpython/lib/test/test_email/test_utils.py +167 -0
  77. synapse/vendor/cpython/lib/test/test_ipaddress.py +2672 -0
  78. synapse/vendor/utils.py +4 -3
  79. {synapse-2.180.1.dist-info → synapse-2.181.0.dist-info}/METADATA +1 -1
  80. {synapse-2.180.1.dist-info → synapse-2.181.0.dist-info}/RECORD +83 -66
  81. {synapse-2.180.1.dist-info → synapse-2.181.0.dist-info}/WHEEL +1 -1
  82. synapse/lib/jupyter.py +0 -505
  83. synapse/tests/test_lib_jupyter.py +0 -224
  84. {synapse-2.180.1.dist-info → synapse-2.181.0.dist-info}/LICENSE +0 -0
  85. {synapse-2.180.1.dist-info → synapse-2.181.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,560 @@
1
+ ##############################################################################
2
+ # Taken from the cpython 3.11 source branch after the 3.11.10 release.
3
+ ##############################################################################
4
+ # Copyright (C) 2002-2007 Python Software Foundation
5
+ # Contact: email-sig@python.org
6
+
7
+ """Email address parsing code.
8
+
9
+ Lifted directly from rfc822.py. This should eventually be rewritten.
10
+ """
11
+
12
+ __all__ = [
13
+ 'mktime_tz',
14
+ 'parsedate',
15
+ 'parsedate_tz',
16
+ 'quote',
17
+ ]
18
+
19
+ import time, calendar
20
+
21
+ SPACE = ' '
22
+ EMPTYSTRING = ''
23
+ COMMASPACE = ', '
24
+
25
+ # Parse a date field
26
+ _monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
27
+ 'aug', 'sep', 'oct', 'nov', 'dec',
28
+ 'january', 'february', 'march', 'april', 'may', 'june', 'july',
29
+ 'august', 'september', 'october', 'november', 'december']
30
+
31
+ _daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
32
+
33
+ # The timezone table does not include the military time zones defined
34
+ # in RFC822, other than Z. According to RFC1123, the description in
35
+ # RFC822 gets the signs wrong, so we can't rely on any such time
36
+ # zones. RFC1123 recommends that numeric timezone indicators be used
37
+ # instead of timezone names.
38
+
39
+ _timezones = {'UT': 0, 'UTC': 0, 'GMT': 0, 'Z': 0,
40
+ 'AST': -400, 'ADT': -300, # Atlantic (used in Canada)
41
+ 'EST': -500, 'EDT': -400, # Eastern
42
+ 'CST': -600, 'CDT': -500, # Central
43
+ 'MST': -700, 'MDT': -600, # Mountain
44
+ 'PST': -800, 'PDT': -700 # Pacific
45
+ }
46
+
47
+
48
+ def parsedate_tz(data):
49
+ """Convert a date string to a time tuple.
50
+
51
+ Accounts for military timezones.
52
+ """
53
+ res = _parsedate_tz(data)
54
+ if not res:
55
+ return
56
+ if res[9] is None:
57
+ res[9] = 0
58
+ return tuple(res)
59
+
60
+ def _parsedate_tz(data):
61
+ """Convert date to extended time tuple.
62
+
63
+ The last (additional) element is the time zone offset in seconds, except if
64
+ the timezone was specified as -0000. In that case the last element is
65
+ None. This indicates a UTC timestamp that explicitly declaims knowledge of
66
+ the source timezone, as opposed to a +0000 timestamp that indicates the
67
+ source timezone really was UTC.
68
+
69
+ """
70
+ if not data:
71
+ return None
72
+ data = data.split()
73
+ if not data: # This happens for whitespace-only input.
74
+ return None
75
+ # The FWS after the comma after the day-of-week is optional, so search and
76
+ # adjust for this.
77
+ if data[0].endswith(',') or data[0].lower() in _daynames:
78
+ # There's a dayname here. Skip it
79
+ del data[0]
80
+ else:
81
+ i = data[0].rfind(',')
82
+ if i >= 0:
83
+ data[0] = data[0][i + 1:]
84
+ if len(data) == 3: # RFC 850 date, deprecated
85
+ stuff = data[0].split('-')
86
+ if len(stuff) == 3:
87
+ data = stuff + data[1:]
88
+ if len(data) == 4:
89
+ s = data[3]
90
+ i = s.find('+')
91
+ if i == -1:
92
+ i = s.find('-')
93
+ if i > 0:
94
+ data[3:] = [s[:i], s[i:]]
95
+ else:
96
+ data.append('') # Dummy tz
97
+ if len(data) < 5:
98
+ return None
99
+ data = data[:5]
100
+ [dd, mm, yy, tm, tz] = data
101
+ if not (dd and mm and yy):
102
+ return None
103
+ mm = mm.lower()
104
+ if mm not in _monthnames:
105
+ dd, mm = mm, dd.lower()
106
+ if mm not in _monthnames:
107
+ return None
108
+ mm = _monthnames.index(mm) + 1
109
+ if mm > 12:
110
+ mm -= 12
111
+ if dd[-1] == ',':
112
+ dd = dd[:-1]
113
+ i = yy.find(':')
114
+ if i > 0:
115
+ yy, tm = tm, yy
116
+ if yy[-1] == ',':
117
+ yy = yy[:-1]
118
+ if not yy:
119
+ return None
120
+ if not yy[0].isdigit():
121
+ yy, tz = tz, yy
122
+ if tm[-1] == ',':
123
+ tm = tm[:-1]
124
+ tm = tm.split(':')
125
+ if len(tm) == 2:
126
+ [thh, tmm] = tm
127
+ tss = '0'
128
+ elif len(tm) == 3:
129
+ [thh, tmm, tss] = tm
130
+ elif len(tm) == 1 and '.' in tm[0]:
131
+ # Some non-compliant MUAs use '.' to separate time elements.
132
+ tm = tm[0].split('.')
133
+ if len(tm) == 2:
134
+ [thh, tmm] = tm
135
+ tss = 0
136
+ elif len(tm) == 3:
137
+ [thh, tmm, tss] = tm
138
+ else:
139
+ return None
140
+ else:
141
+ return None
142
+ try:
143
+ yy = int(yy)
144
+ dd = int(dd)
145
+ thh = int(thh)
146
+ tmm = int(tmm)
147
+ tss = int(tss)
148
+ except ValueError:
149
+ return None
150
+ # Check for a yy specified in two-digit format, then convert it to the
151
+ # appropriate four-digit format, according to the POSIX standard. RFC 822
152
+ # calls for a two-digit yy, but RFC 2822 (which obsoletes RFC 822)
153
+ # mandates a 4-digit yy. For more information, see the documentation for
154
+ # the time module.
155
+ if yy < 100:
156
+ # The year is between 1969 and 1999 (inclusive).
157
+ if yy > 68:
158
+ yy += 1900
159
+ # The year is between 2000 and 2068 (inclusive).
160
+ else:
161
+ yy += 2000
162
+ tzoffset = None
163
+ tz = tz.upper()
164
+ if tz in _timezones:
165
+ tzoffset = _timezones[tz]
166
+ else:
167
+ try:
168
+ tzoffset = int(tz)
169
+ except ValueError:
170
+ pass
171
+ if tzoffset == 0 and tz.startswith('-'):
172
+ tzoffset = None
173
+ # Convert a timezone offset into seconds ; -0500 -> -18000
174
+ if tzoffset:
175
+ if tzoffset < 0:
176
+ tzsign = -1
177
+ tzoffset = -tzoffset
178
+ else:
179
+ tzsign = 1
180
+ tzoffset = tzsign * ((tzoffset // 100) * 3600 + (tzoffset % 100) * 60)
181
+ # Daylight Saving Time flag is set to -1, since DST is unknown.
182
+ return [yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset]
183
+
184
+
185
+ def parsedate(data):
186
+ """Convert a time string to a time tuple."""
187
+ t = parsedate_tz(data)
188
+ if isinstance(t, tuple):
189
+ return t[:9]
190
+ else:
191
+ return t
192
+
193
+
194
+ def mktime_tz(data):
195
+ """Turn a 10-tuple as returned by parsedate_tz() into a POSIX timestamp."""
196
+ if data[9] is None:
197
+ # No zone info, so localtime is better assumption than GMT
198
+ return time.mktime(data[:8] + (-1,))
199
+ else:
200
+ t = calendar.timegm(data)
201
+ return t - data[9]
202
+
203
+
204
+ def quote(str):
205
+ """Prepare string to be used in a quoted string.
206
+
207
+ Turns backslash and double quote characters into quoted pairs. These
208
+ are the only characters that need to be quoted inside a quoted string.
209
+ Does not add the surrounding double quotes.
210
+ """
211
+ return str.replace('\\', '\\\\').replace('"', '\\"')
212
+
213
+
214
+ class AddrlistClass:
215
+ """Address parser class by Ben Escoto.
216
+
217
+ To understand what this class does, it helps to have a copy of RFC 2822 in
218
+ front of you.
219
+
220
+ Note: this class interface is deprecated and may be removed in the future.
221
+ Use email.utils.AddressList instead.
222
+ """
223
+
224
+ def __init__(self, field):
225
+ """Initialize a new instance.
226
+
227
+ `field' is an unparsed address header field, containing
228
+ one or more addresses.
229
+ """
230
+ self.specials = '()<>@,:;.\"[]'
231
+ self.pos = 0
232
+ self.LWS = ' \t'
233
+ self.CR = '\r\n'
234
+ self.FWS = self.LWS + self.CR
235
+ self.atomends = self.specials + self.LWS + self.CR
236
+ # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
237
+ # is obsolete syntax. RFC 2822 requires that we recognize obsolete
238
+ # syntax, so allow dots in phrases.
239
+ self.phraseends = self.atomends.replace('.', '')
240
+ self.field = field
241
+ self.commentlist = []
242
+
243
+ def gotonext(self):
244
+ """Skip white space and extract comments."""
245
+ wslist = []
246
+ while self.pos < len(self.field):
247
+ if self.field[self.pos] in self.LWS + '\n\r':
248
+ if self.field[self.pos] not in '\n\r':
249
+ wslist.append(self.field[self.pos])
250
+ self.pos += 1
251
+ elif self.field[self.pos] == '(':
252
+ self.commentlist.append(self.getcomment())
253
+ else:
254
+ break
255
+ return EMPTYSTRING.join(wslist)
256
+
257
+ def getaddrlist(self):
258
+ """Parse all addresses.
259
+
260
+ Returns a list containing all of the addresses.
261
+ """
262
+ result = []
263
+ while self.pos < len(self.field):
264
+ ad = self.getaddress()
265
+ if ad:
266
+ result += ad
267
+ else:
268
+ result.append(('', ''))
269
+ return result
270
+
271
+ def getaddress(self):
272
+ """Parse the next address."""
273
+ self.commentlist = []
274
+ self.gotonext()
275
+
276
+ oldpos = self.pos
277
+ oldcl = self.commentlist
278
+ plist = self.getphraselist()
279
+
280
+ self.gotonext()
281
+ returnlist = []
282
+
283
+ if self.pos >= len(self.field):
284
+ # Bad email address technically, no domain.
285
+ if plist:
286
+ returnlist = [(SPACE.join(self.commentlist), plist[0])]
287
+
288
+ elif self.field[self.pos] in '.@':
289
+ # email address is just an addrspec
290
+ # this isn't very efficient since we start over
291
+ self.pos = oldpos
292
+ self.commentlist = oldcl
293
+ addrspec = self.getaddrspec()
294
+ returnlist = [(SPACE.join(self.commentlist), addrspec)]
295
+
296
+ elif self.field[self.pos] == ':':
297
+ # address is a group
298
+ returnlist = []
299
+
300
+ fieldlen = len(self.field)
301
+ self.pos += 1
302
+ while self.pos < len(self.field):
303
+ self.gotonext()
304
+ if self.pos < fieldlen and self.field[self.pos] == ';':
305
+ self.pos += 1
306
+ break
307
+ returnlist = returnlist + self.getaddress()
308
+
309
+ elif self.field[self.pos] == '<':
310
+ # Address is a phrase then a route addr
311
+ routeaddr = self.getrouteaddr()
312
+
313
+ if self.commentlist:
314
+ returnlist = [(SPACE.join(plist) + ' (' +
315
+ ' '.join(self.commentlist) + ')', routeaddr)]
316
+ else:
317
+ returnlist = [(SPACE.join(plist), routeaddr)]
318
+
319
+ else:
320
+ if plist:
321
+ returnlist = [(SPACE.join(self.commentlist), plist[0])]
322
+ elif self.field[self.pos] in self.specials:
323
+ self.pos += 1
324
+
325
+ self.gotonext()
326
+ if self.pos < len(self.field) and self.field[self.pos] == ',':
327
+ self.pos += 1
328
+ return returnlist
329
+
330
+ def getrouteaddr(self):
331
+ """Parse a route address (Return-path value).
332
+
333
+ This method just skips all the route stuff and returns the addrspec.
334
+ """
335
+ if self.field[self.pos] != '<':
336
+ return
337
+
338
+ expectroute = False
339
+ self.pos += 1
340
+ self.gotonext()
341
+ adlist = ''
342
+ while self.pos < len(self.field):
343
+ if expectroute:
344
+ self.getdomain()
345
+ expectroute = False
346
+ elif self.field[self.pos] == '>':
347
+ self.pos += 1
348
+ break
349
+ elif self.field[self.pos] == '@':
350
+ self.pos += 1
351
+ expectroute = True
352
+ elif self.field[self.pos] == ':':
353
+ self.pos += 1
354
+ else:
355
+ adlist = self.getaddrspec()
356
+ self.pos += 1
357
+ break
358
+ self.gotonext()
359
+
360
+ return adlist
361
+
362
+ def getaddrspec(self):
363
+ """Parse an RFC 2822 addr-spec."""
364
+ aslist = []
365
+
366
+ self.gotonext()
367
+ while self.pos < len(self.field):
368
+ preserve_ws = True
369
+ if self.field[self.pos] == '.':
370
+ if aslist and not aslist[-1].strip():
371
+ aslist.pop()
372
+ aslist.append('.')
373
+ self.pos += 1
374
+ preserve_ws = False
375
+ elif self.field[self.pos] == '"':
376
+ aslist.append('"%s"' % quote(self.getquote()))
377
+ elif self.field[self.pos] in self.atomends:
378
+ if aslist and not aslist[-1].strip():
379
+ aslist.pop()
380
+ break
381
+ else:
382
+ aslist.append(self.getatom())
383
+ ws = self.gotonext()
384
+ if preserve_ws and ws:
385
+ aslist.append(ws)
386
+
387
+ if self.pos >= len(self.field) or self.field[self.pos] != '@':
388
+ return EMPTYSTRING.join(aslist)
389
+
390
+ aslist.append('@')
391
+ self.pos += 1
392
+ self.gotonext()
393
+ domain = self.getdomain()
394
+ if not domain:
395
+ # Invalid domain, return an empty address instead of returning a
396
+ # local part to denote failed parsing.
397
+ return EMPTYSTRING
398
+ return EMPTYSTRING.join(aslist) + domain
399
+
400
+ def getdomain(self):
401
+ """Get the complete domain name from an address."""
402
+ sdlist = []
403
+ while self.pos < len(self.field):
404
+ if self.field[self.pos] in self.LWS:
405
+ self.pos += 1
406
+ elif self.field[self.pos] == '(':
407
+ self.commentlist.append(self.getcomment())
408
+ elif self.field[self.pos] == '[':
409
+ sdlist.append(self.getdomainliteral())
410
+ elif self.field[self.pos] == '.':
411
+ self.pos += 1
412
+ sdlist.append('.')
413
+ elif self.field[self.pos] == '@':
414
+ # bpo-34155: Don't parse domains with two `@` like
415
+ # `a@malicious.org@important.com`.
416
+ return EMPTYSTRING
417
+ elif self.field[self.pos] in self.atomends:
418
+ break
419
+ else:
420
+ sdlist.append(self.getatom())
421
+ return EMPTYSTRING.join(sdlist)
422
+
423
+ def getdelimited(self, beginchar, endchars, allowcomments=True):
424
+ """Parse a header fragment delimited by special characters.
425
+
426
+ `beginchar' is the start character for the fragment.
427
+ If self is not looking at an instance of `beginchar' then
428
+ getdelimited returns the empty string.
429
+
430
+ `endchars' is a sequence of allowable end-delimiting characters.
431
+ Parsing stops when one of these is encountered.
432
+
433
+ If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
434
+ within the parsed fragment.
435
+ """
436
+ if self.field[self.pos] != beginchar:
437
+ return ''
438
+
439
+ slist = ['']
440
+ quote = False
441
+ self.pos += 1
442
+ while self.pos < len(self.field):
443
+ if quote:
444
+ slist.append(self.field[self.pos])
445
+ quote = False
446
+ elif self.field[self.pos] in endchars:
447
+ self.pos += 1
448
+ break
449
+ elif allowcomments and self.field[self.pos] == '(':
450
+ slist.append(self.getcomment())
451
+ continue # have already advanced pos from getcomment
452
+ elif self.field[self.pos] == '\\':
453
+ quote = True
454
+ else:
455
+ slist.append(self.field[self.pos])
456
+ self.pos += 1
457
+
458
+ return EMPTYSTRING.join(slist)
459
+
460
+ def getquote(self):
461
+ """Get a quote-delimited fragment from self's field."""
462
+ return self.getdelimited('"', '"\r', False)
463
+
464
+ def getcomment(self):
465
+ """Get a parenthesis-delimited fragment from self's field."""
466
+ return self.getdelimited('(', ')\r', True)
467
+
468
+ def getdomainliteral(self):
469
+ """Parse an RFC 2822 domain-literal."""
470
+ return '[%s]' % self.getdelimited('[', ']\r', False)
471
+
472
+ def getatom(self, atomends=None):
473
+ """Parse an RFC 2822 atom.
474
+
475
+ Optional atomends specifies a different set of end token delimiters
476
+ (the default is to use self.atomends). This is used e.g. in
477
+ getphraselist() since phrase endings must not include the `.' (which
478
+ is legal in phrases)."""
479
+ atomlist = ['']
480
+ if atomends is None:
481
+ atomends = self.atomends
482
+
483
+ while self.pos < len(self.field):
484
+ if self.field[self.pos] in atomends:
485
+ break
486
+ else:
487
+ atomlist.append(self.field[self.pos])
488
+ self.pos += 1
489
+
490
+ return EMPTYSTRING.join(atomlist)
491
+
492
+ def getphraselist(self):
493
+ """Parse a sequence of RFC 2822 phrases.
494
+
495
+ A phrase is a sequence of words, which are in turn either RFC 2822
496
+ atoms or quoted-strings. Phrases are canonicalized by squeezing all
497
+ runs of continuous whitespace into one space.
498
+ """
499
+ plist = []
500
+
501
+ while self.pos < len(self.field):
502
+ if self.field[self.pos] in self.FWS:
503
+ self.pos += 1
504
+ elif self.field[self.pos] == '"':
505
+ plist.append(self.getquote())
506
+ elif self.field[self.pos] == '(':
507
+ self.commentlist.append(self.getcomment())
508
+ elif self.field[self.pos] in self.phraseends:
509
+ break
510
+ else:
511
+ plist.append(self.getatom(self.phraseends))
512
+
513
+ return plist
514
+
515
+ class AddressList(AddrlistClass):
516
+ """An AddressList encapsulates a list of parsed RFC 2822 addresses."""
517
+ def __init__(self, field):
518
+ AddrlistClass.__init__(self, field)
519
+ if field:
520
+ self.addresslist = self.getaddrlist()
521
+ else:
522
+ self.addresslist = []
523
+
524
+ def __len__(self):
525
+ return len(self.addresslist)
526
+
527
+ def __add__(self, other):
528
+ # Set union
529
+ newaddr = AddressList(None)
530
+ newaddr.addresslist = self.addresslist[:]
531
+ for x in other.addresslist:
532
+ if x not in self.addresslist:
533
+ newaddr.addresslist.append(x)
534
+ return newaddr
535
+
536
+ def __iadd__(self, other):
537
+ # Set union, in-place
538
+ for x in other.addresslist:
539
+ if x not in self.addresslist:
540
+ self.addresslist.append(x)
541
+ return self
542
+
543
+ def __sub__(self, other):
544
+ # Set difference
545
+ newaddr = AddressList(None)
546
+ for x in self.addresslist:
547
+ if x not in other.addresslist:
548
+ newaddr.addresslist.append(x)
549
+ return newaddr
550
+
551
+ def __isub__(self, other):
552
+ # Set difference, in-place
553
+ for x in other.addresslist:
554
+ if x in self.addresslist:
555
+ self.addresslist.remove(x)
556
+ return self
557
+
558
+ def __getitem__(self, index):
559
+ # Make indexing, slices, and 'in' work
560
+ return self.addresslist[index]