synapse 2.180.1__py311-none-any.whl → 2.182.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 (90) 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/auth.py +17 -0
  9. synapse/lib/cell.py +7 -9
  10. synapse/lib/chop.py +0 -1
  11. synapse/lib/drive.py +8 -8
  12. synapse/lib/layer.py +55 -13
  13. synapse/lib/lmdbslab.py +26 -5
  14. synapse/lib/modelrev.py +28 -1
  15. synapse/lib/modules.py +1 -0
  16. synapse/lib/nexus.py +1 -1
  17. synapse/lib/node.py +5 -0
  18. synapse/lib/parser.py +23 -16
  19. synapse/lib/scrape.py +1 -1
  20. synapse/lib/slabseqn.py +2 -2
  21. synapse/lib/snap.py +129 -0
  22. synapse/lib/storm.lark +16 -2
  23. synapse/lib/storm.py +20 -3
  24. synapse/lib/storm_format.py +1 -0
  25. synapse/lib/stormhttp.py +34 -1
  26. synapse/lib/stormlib/auth.py +5 -3
  27. synapse/lib/stormlib/cortex.py +5 -2
  28. synapse/lib/stormlib/easyperm.py +2 -2
  29. synapse/lib/stormlib/ipv6.py +2 -2
  30. synapse/lib/stormlib/model.py +114 -12
  31. synapse/lib/stormlib/project.py +1 -1
  32. synapse/lib/stormtypes.py +81 -7
  33. synapse/lib/types.py +7 -0
  34. synapse/lib/version.py +2 -2
  35. synapse/lib/view.py +47 -0
  36. synapse/models/inet.py +10 -3
  37. synapse/models/infotech.py +2 -1
  38. synapse/models/language.py +4 -0
  39. synapse/models/math.py +50 -0
  40. synapse/models/orgs.py +8 -0
  41. synapse/models/risk.py +9 -0
  42. synapse/tests/files/stormcov/pragma-nocov.storm +18 -0
  43. synapse/tests/test_assets.py +25 -0
  44. synapse/tests/test_cortex.py +129 -0
  45. synapse/tests/test_datamodel.py +6 -0
  46. synapse/tests/test_lib_cell.py +12 -0
  47. synapse/tests/test_lib_grammar.py +7 -1
  48. synapse/tests/test_lib_layer.py +35 -0
  49. synapse/tests/test_lib_lmdbslab.py +11 -9
  50. synapse/tests/test_lib_modelrev.py +655 -1
  51. synapse/tests/test_lib_slabseqn.py +5 -4
  52. synapse/tests/test_lib_snap.py +4 -0
  53. synapse/tests/test_lib_storm.py +110 -1
  54. synapse/tests/test_lib_stormhttp.py +99 -1
  55. synapse/tests/test_lib_stormlib_auth.py +15 -0
  56. synapse/tests/test_lib_stormlib_cortex.py +21 -4
  57. synapse/tests/test_lib_stormlib_iters.py +8 -5
  58. synapse/tests/test_lib_stormlib_model.py +45 -6
  59. synapse/tests/test_lib_stormtypes.py +158 -2
  60. synapse/tests/test_lib_types.py +6 -0
  61. synapse/tests/test_model_inet.py +10 -0
  62. synapse/tests/test_model_language.py +4 -0
  63. synapse/tests/test_model_math.py +22 -0
  64. synapse/tests/test_model_orgs.py +6 -2
  65. synapse/tests/test_model_risk.py +4 -0
  66. synapse/tests/test_tools_storm.py +1 -1
  67. synapse/tests/test_utils_stormcov.py +5 -0
  68. synapse/tests/utils.py +18 -5
  69. synapse/utils/stormcov/plugin.py +31 -1
  70. synapse/vendor/cpython/LICENSE +279 -0
  71. synapse/vendor/cpython/__init__.py +0 -0
  72. synapse/vendor/cpython/lib/__init__.py +0 -0
  73. synapse/vendor/cpython/lib/email/__init__.py +0 -0
  74. synapse/vendor/cpython/lib/email/_parseaddr.py +560 -0
  75. synapse/vendor/cpython/lib/email/utils.py +505 -0
  76. synapse/vendor/cpython/lib/ipaddress.py +2366 -0
  77. synapse/vendor/cpython/lib/test/__init__.py +0 -0
  78. synapse/vendor/cpython/lib/test/support/__init__.py +114 -0
  79. synapse/vendor/cpython/lib/test/test_email/__init__.py +0 -0
  80. synapse/vendor/cpython/lib/test/test_email/test_email.py +480 -0
  81. synapse/vendor/cpython/lib/test/test_email/test_utils.py +167 -0
  82. synapse/vendor/cpython/lib/test/test_ipaddress.py +2672 -0
  83. synapse/vendor/utils.py +4 -3
  84. {synapse-2.180.1.dist-info → synapse-2.182.0.dist-info}/METADATA +3 -3
  85. {synapse-2.180.1.dist-info → synapse-2.182.0.dist-info}/RECORD +88 -71
  86. {synapse-2.180.1.dist-info → synapse-2.182.0.dist-info}/WHEEL +1 -1
  87. synapse/lib/jupyter.py +0 -505
  88. synapse/tests/test_lib_jupyter.py +0 -224
  89. {synapse-2.180.1.dist-info → synapse-2.182.0.dist-info}/LICENSE +0 -0
  90. {synapse-2.180.1.dist-info → synapse-2.182.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,505 @@
1
+ ##############################################################################
2
+ # Taken from the cpython 3.11 source branch after the 3.11.10 release.
3
+ ##############################################################################
4
+ # Copyright (C) 2001-2010 Python Software Foundation
5
+ # Author: Barry Warsaw
6
+ # Contact: email-sig@python.org
7
+
8
+ """Miscellaneous utilities."""
9
+
10
+ __all__ = [
11
+ # 'collapse_rfc2231_value',
12
+ # 'decode_params',
13
+ # 'decode_rfc2231',
14
+ # 'encode_rfc2231',
15
+ 'formataddr',
16
+ 'formatdate',
17
+ 'format_datetime',
18
+ 'getaddresses',
19
+ 'make_msgid',
20
+ 'mktime_tz',
21
+ 'parseaddr',
22
+ 'parsedate',
23
+ 'parsedate_tz',
24
+ 'parsedate_to_datetime',
25
+ 'unquote',
26
+ ]
27
+
28
+ import os
29
+ import re
30
+ import time
31
+ import random
32
+ import socket
33
+ import datetime
34
+ import urllib.parse
35
+
36
+ from synapse.vendor.cpython.lib.email._parseaddr import quote
37
+ from synapse.vendor.cpython.lib.email._parseaddr import AddressList as _AddressList
38
+ from synapse.vendor.cpython.lib.email._parseaddr import mktime_tz
39
+
40
+ from synapse.vendor.cpython.lib.email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
41
+
42
+ # Intrapackage imports
43
+ from email.charset import Charset
44
+
45
+ COMMASPACE = ', '
46
+ EMPTYSTRING = ''
47
+ UEMPTYSTRING = ''
48
+ CRLF = '\r\n'
49
+ TICK = "'"
50
+
51
+ specialsre = re.compile(r'[][\\()<>@,:;".]')
52
+ escapesre = re.compile(r'[\\"]')
53
+
54
+
55
+ def _has_surrogates(s):
56
+ """Return True if s may contain surrogate-escaped binary data."""
57
+ # This check is based on the fact that unless there are surrogates, utf8
58
+ # (Python's default encoding) can encode any string. This is the fastest
59
+ # way to check for surrogates, see bpo-11454 (moved to gh-55663) for timings.
60
+ try:
61
+ s.encode()
62
+ return False
63
+ except UnicodeEncodeError:
64
+ return True
65
+
66
+ # How to deal with a string containing bytes before handing it to the
67
+ # application through the 'normal' interface.
68
+ def _sanitize(string):
69
+ # Turn any escaped bytes into unicode 'unknown' char. If the escaped
70
+ # bytes happen to be utf-8 they will instead get decoded, even if they
71
+ # were invalid in the charset the source was supposed to be in. This
72
+ # seems like it is not a bad thing; a defect was still registered.
73
+ original_bytes = string.encode('utf-8', 'surrogateescape')
74
+ return original_bytes.decode('utf-8', 'replace')
75
+
76
+
77
+ # Helpers
78
+
79
+ def formataddr(pair, charset='utf-8'):
80
+ """The inverse of parseaddr(), this takes a 2-tuple of the form
81
+ (realname, email_address) and returns the string value suitable
82
+ for an RFC 2822 From, To or Cc header.
83
+
84
+ If the first element of pair is false, then the second element is
85
+ returned unmodified.
86
+
87
+ The optional charset is the character set that is used to encode
88
+ realname in case realname is not ASCII safe. Can be an instance of str or
89
+ a Charset-like object which has a header_encode method. Default is
90
+ 'utf-8'.
91
+ """
92
+ name, address = pair
93
+ # The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
94
+ address.encode('ascii')
95
+ if name:
96
+ try:
97
+ name.encode('ascii')
98
+ except UnicodeEncodeError:
99
+ if isinstance(charset, str):
100
+ charset = Charset(charset)
101
+ encoded_name = charset.header_encode(name)
102
+ return "%s <%s>" % (encoded_name, address)
103
+ else:
104
+ quotes = ''
105
+ if specialsre.search(name):
106
+ quotes = '"'
107
+ name = escapesre.sub(r'\\\g<0>', name)
108
+ return '%s%s%s <%s>' % (quotes, name, quotes, address)
109
+ return address
110
+
111
+
112
+ def _iter_escaped_chars(addr):
113
+ pos = 0
114
+ escape = False
115
+ for pos, ch in enumerate(addr):
116
+ if escape:
117
+ yield (pos, '\\' + ch)
118
+ escape = False
119
+ elif ch == '\\':
120
+ escape = True
121
+ else:
122
+ yield (pos, ch)
123
+ if escape:
124
+ yield (pos, '\\')
125
+
126
+
127
+ def _strip_quoted_realnames(addr):
128
+ """Strip real names between quotes."""
129
+ if '"' not in addr:
130
+ # Fast path
131
+ return addr
132
+
133
+ start = 0
134
+ open_pos = None
135
+ result = []
136
+ for pos, ch in _iter_escaped_chars(addr):
137
+ if ch == '"':
138
+ if open_pos is None:
139
+ open_pos = pos
140
+ else:
141
+ if start != open_pos:
142
+ result.append(addr[start:open_pos])
143
+ start = pos + 1
144
+ open_pos = None
145
+
146
+ if start < len(addr):
147
+ result.append(addr[start:])
148
+
149
+ return ''.join(result)
150
+
151
+
152
+ supports_strict_parsing = True
153
+
154
+ def getaddresses(fieldvalues, *, strict=True):
155
+ """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
156
+
157
+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
158
+ its place.
159
+
160
+ If strict is true, use a strict parser which rejects malformed inputs.
161
+ """
162
+
163
+ # If strict is true, if the resulting list of parsed addresses is greater
164
+ # than the number of fieldvalues in the input list, a parsing error has
165
+ # occurred and consequently a list containing a single empty 2-tuple [('',
166
+ # '')] is returned in its place. This is done to avoid invalid output.
167
+ #
168
+ # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
169
+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
170
+ # Safe output: [('', '')]
171
+
172
+ if not strict:
173
+ all = COMMASPACE.join(str(v) for v in fieldvalues)
174
+ a = _AddressList(all)
175
+ return a.addresslist
176
+
177
+ fieldvalues = [str(v) for v in fieldvalues]
178
+ fieldvalues = _pre_parse_validation(fieldvalues)
179
+ addr = COMMASPACE.join(fieldvalues)
180
+ a = _AddressList(addr)
181
+ result = _post_parse_validation(a.addresslist)
182
+
183
+ # Treat output as invalid if the number of addresses is not equal to the
184
+ # expected number of addresses.
185
+ n = 0
186
+ for v in fieldvalues:
187
+ # When a comma is used in the Real Name part it is not a deliminator.
188
+ # So strip those out before counting the commas.
189
+ v = _strip_quoted_realnames(v)
190
+ # Expected number of addresses: 1 + number of commas
191
+ n += 1 + v.count(',')
192
+ if len(result) != n:
193
+ return [('', '')]
194
+
195
+ return result
196
+
197
+
198
+ def _check_parenthesis(addr):
199
+ # Ignore parenthesis in quoted real names.
200
+ addr = _strip_quoted_realnames(addr)
201
+
202
+ opens = 0
203
+ for pos, ch in _iter_escaped_chars(addr):
204
+ if ch == '(':
205
+ opens += 1
206
+ elif ch == ')':
207
+ opens -= 1
208
+ if opens < 0:
209
+ return False
210
+ return (opens == 0)
211
+
212
+
213
+ def _pre_parse_validation(email_header_fields):
214
+ accepted_values = []
215
+ for v in email_header_fields:
216
+ if not _check_parenthesis(v):
217
+ v = "('', '')"
218
+ accepted_values.append(v)
219
+
220
+ return accepted_values
221
+
222
+
223
+ def _post_parse_validation(parsed_email_header_tuples):
224
+ accepted_values = []
225
+ # The parser would have parsed a correctly formatted domain-literal
226
+ # The existence of an [ after parsing indicates a parsing failure
227
+ for v in parsed_email_header_tuples:
228
+ if '[' in v[1]:
229
+ v = ('', '')
230
+ accepted_values.append(v)
231
+
232
+ return accepted_values
233
+
234
+
235
+ def _format_timetuple_and_zone(timetuple, zone):
236
+ return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
237
+ ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]],
238
+ timetuple[2],
239
+ ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
240
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1],
241
+ timetuple[0], timetuple[3], timetuple[4], timetuple[5],
242
+ zone)
243
+
244
+ def formatdate(timeval=None, localtime=False, usegmt=False):
245
+ """Returns a date string as specified by RFC 2822, e.g.:
246
+
247
+ Fri, 09 Nov 2001 01:08:47 -0000
248
+
249
+ Optional timeval if given is a floating point time value as accepted by
250
+ gmtime() and localtime(), otherwise the current time is used.
251
+
252
+ Optional localtime is a flag that when True, interprets timeval, and
253
+ returns a date relative to the local timezone instead of UTC, properly
254
+ taking daylight savings time into account.
255
+
256
+ Optional argument usegmt means that the timezone is written out as
257
+ an ascii string, not numeric one (so "GMT" instead of "+0000"). This
258
+ is needed for HTTP, and is only used when localtime==False.
259
+ """
260
+ # Note: we cannot use strftime() because that honors the locale and RFC
261
+ # 2822 requires that day and month names be the English abbreviations.
262
+ if timeval is None:
263
+ timeval = time.time()
264
+ if localtime or usegmt:
265
+ dt = datetime.datetime.fromtimestamp(timeval, datetime.timezone.utc)
266
+ else:
267
+ dt = datetime.datetime.utcfromtimestamp(timeval)
268
+ if localtime:
269
+ dt = dt.astimezone()
270
+ usegmt = False
271
+ return format_datetime(dt, usegmt)
272
+
273
+ def format_datetime(dt, usegmt=False):
274
+ """Turn a datetime into a date string as specified in RFC 2822.
275
+
276
+ If usegmt is True, dt must be an aware datetime with an offset of zero. In
277
+ this case 'GMT' will be rendered instead of the normal +0000 required by
278
+ RFC2822. This is to support HTTP headers involving date stamps.
279
+ """
280
+ now = dt.timetuple()
281
+ if usegmt:
282
+ if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc:
283
+ raise ValueError("usegmt option requires a UTC datetime")
284
+ zone = 'GMT'
285
+ elif dt.tzinfo is None:
286
+ zone = '-0000'
287
+ else:
288
+ zone = dt.strftime("%z")
289
+ return _format_timetuple_and_zone(now, zone)
290
+
291
+
292
+ def make_msgid(idstring=None, domain=None):
293
+ """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
294
+
295
+ <142480216486.20800.16526388040877946887@nightshade.la.mastaler.com>
296
+
297
+ Optional idstring if given is a string used to strengthen the
298
+ uniqueness of the message id. Optional domain if given provides the
299
+ portion of the message id after the '@'. It defaults to the locally
300
+ defined hostname.
301
+ """
302
+ timeval = int(time.time() * 100)
303
+ pid = os.getpid()
304
+ randint = random.getrandbits(64)
305
+ if idstring is None:
306
+ idstring = ''
307
+ else:
308
+ idstring = '.' + idstring
309
+ if domain is None:
310
+ domain = socket.getfqdn()
311
+ msgid = '<%d.%d.%d%s@%s>' % (timeval, pid, randint, idstring, domain)
312
+ return msgid
313
+
314
+
315
+ def parsedate_to_datetime(data):
316
+ parsed_date_tz = _parsedate_tz(data)
317
+ if parsed_date_tz is None:
318
+ raise ValueError('Invalid date value or format "%s"' % str(data))
319
+ *dtuple, tz = parsed_date_tz
320
+ if tz is None:
321
+ return datetime.datetime(*dtuple[:6])
322
+ return datetime.datetime(*dtuple[:6],
323
+ tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
324
+
325
+
326
+ def parseaddr(addr, *, strict=True):
327
+ """
328
+ Parse addr into its constituent realname and email address parts.
329
+
330
+ Return a tuple of realname and email address, unless the parse fails, in
331
+ which case return a 2-tuple of ('', '').
332
+
333
+ If strict is True, use a strict parser which rejects malformed inputs.
334
+ """
335
+ if not strict:
336
+ addrs = _AddressList(addr).addresslist
337
+ if not addrs:
338
+ return ('', '')
339
+ return addrs[0]
340
+
341
+ if isinstance(addr, list):
342
+ addr = addr[0]
343
+
344
+ if not isinstance(addr, str):
345
+ return ('', '')
346
+
347
+ addr = _pre_parse_validation([addr])[0]
348
+ addrs = _post_parse_validation(_AddressList(addr).addresslist)
349
+
350
+ if not addrs or len(addrs) > 1:
351
+ return ('', '')
352
+
353
+ return addrs[0]
354
+
355
+
356
+ # rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
357
+ def unquote(str):
358
+ """Remove quotes from a string."""
359
+ if len(str) > 1:
360
+ if str.startswith('"') and str.endswith('"'):
361
+ return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
362
+ if str.startswith('<') and str.endswith('>'):
363
+ return str[1:-1]
364
+ return str
365
+
366
+
367
+ # RFC2231-related functions - parameter encoding and decoding
368
+ # def decode_rfc2231(s):
369
+ # """Decode string according to RFC 2231"""
370
+ # parts = s.split(TICK, 2)
371
+ # if len(parts) <= 2:
372
+ # return None, None, s
373
+ # return parts
374
+
375
+
376
+ # def encode_rfc2231(s, charset=None, language=None):
377
+ # """Encode string according to RFC 2231.
378
+ #
379
+ # If neither charset nor language is given, then s is returned as-is. If
380
+ # charset is given but not language, the string is encoded using the empty
381
+ # string for language.
382
+ # """
383
+ # s = urllib.parse.quote(s, safe='', encoding=charset or 'ascii')
384
+ # if charset is None and language is None:
385
+ # return s
386
+ # if language is None:
387
+ # language = ''
388
+ # return "%s'%s'%s" % (charset, language, s)
389
+
390
+
391
+ rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
392
+ re.ASCII)
393
+
394
+ # def decode_params(params):
395
+ # """Decode parameters list according to RFC 2231.
396
+ #
397
+ # params is a sequence of 2-tuples containing (param name, string value).
398
+ # """
399
+ # new_params = [params[0]]
400
+ # # Map parameter's name to a list of continuations. The values are a
401
+ # # 3-tuple of the continuation number, the string value, and a flag
402
+ # # specifying whether a particular segment is %-encoded.
403
+ # rfc2231_params = {}
404
+ # for name, value in params[1:]:
405
+ # encoded = name.endswith('*')
406
+ # value = unquote(value)
407
+ # mo = rfc2231_continuation.match(name)
408
+ # if mo:
409
+ # name, num = mo.group('name', 'num')
410
+ # if num is not None:
411
+ # num = int(num)
412
+ # rfc2231_params.setdefault(name, []).append((num, value, encoded))
413
+ # else:
414
+ # new_params.append((name, '"%s"' % quote(value)))
415
+ # if rfc2231_params:
416
+ # for name, continuations in rfc2231_params.items():
417
+ # value = []
418
+ # extended = False
419
+ # # Sort by number
420
+ # continuations.sort()
421
+ # # And now append all values in numerical order, converting
422
+ # # %-encodings for the encoded segments. If any of the
423
+ # # continuation names ends in a *, then the entire string, after
424
+ # # decoding segments and concatenating, must have the charset and
425
+ # # language specifiers at the beginning of the string.
426
+ # for num, s, encoded in continuations:
427
+ # if encoded:
428
+ # # Decode as "latin-1", so the characters in s directly
429
+ # # represent the percent-encoded octet values.
430
+ # # collapse_rfc2231_value treats this as an octet sequence.
431
+ # s = urllib.parse.unquote(s, encoding="latin-1")
432
+ # extended = True
433
+ # value.append(s)
434
+ # value = quote(EMPTYSTRING.join(value))
435
+ # if extended:
436
+ # charset, language, value = decode_rfc2231(value)
437
+ # new_params.append((name, (charset, language, '"%s"' % value)))
438
+ # else:
439
+ # new_params.append((name, '"%s"' % value))
440
+ # return new_params
441
+
442
+ # def collapse_rfc2231_value(value, errors='replace',
443
+ # fallback_charset='us-ascii'):
444
+ # if not isinstance(value, tuple) or len(value) != 3:
445
+ # return unquote(value)
446
+ # # While value comes to us as a unicode string, we need it to be a bytes
447
+ # # object. We do not want bytes() normal utf-8 decoder, we want a straight
448
+ # # interpretation of the string as character bytes.
449
+ # charset, language, text = value
450
+ # if charset is None:
451
+ # # Issue 17369: if charset/lang is None, decode_rfc2231 couldn't parse
452
+ # # the value, so use the fallback_charset.
453
+ # charset = fallback_charset
454
+ # rawbytes = bytes(text, 'raw-unicode-escape')
455
+ # try:
456
+ # return str(rawbytes, charset, errors)
457
+ # except LookupError:
458
+ # # charset is not a known codec.
459
+ # return unquote(text)
460
+
461
+
462
+ #
463
+ # datetime doesn't provide a localtime function yet, so provide one. Code
464
+ # adapted from the patch in issue 9527. This may not be perfect, but it is
465
+ # better than not having it.
466
+ #
467
+
468
+ def localtime(dt=None, isdst=-1):
469
+ """Return local time as an aware datetime object.
470
+
471
+ If called without arguments, return current time. Otherwise *dt*
472
+ argument should be a datetime instance, and it is converted to the
473
+ local time zone according to the system time zone database. If *dt* is
474
+ naive (that is, dt.tzinfo is None), it is assumed to be in local time.
475
+ In this case, a positive or zero value for *isdst* causes localtime to
476
+ presume initially that summer time (for example, Daylight Saving Time)
477
+ is or is not (respectively) in effect for the specified time. A
478
+ negative value for *isdst* causes the localtime() function to attempt
479
+ to divine whether summer time is in effect for the specified time.
480
+
481
+ """
482
+ if dt is None:
483
+ return datetime.datetime.now(datetime.timezone.utc).astimezone()
484
+ if dt.tzinfo is not None:
485
+ return dt.astimezone()
486
+ # We have a naive datetime. Convert to a (localtime) timetuple and pass to
487
+ # system mktime together with the isdst hint. System mktime will return
488
+ # seconds since epoch.
489
+ tm = dt.timetuple()[:-1] + (isdst,)
490
+ seconds = time.mktime(tm)
491
+ localtm = time.localtime(seconds)
492
+ try:
493
+ delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
494
+ tz = datetime.timezone(delta, localtm.tm_zone)
495
+ except AttributeError:
496
+ # Compute UTC offset and compare with the value implied by tm_isdst.
497
+ # If the values match, use the zone name implied by tm_isdst.
498
+ delta = dt - datetime.datetime(*time.gmtime(seconds)[:6])
499
+ dst = time.daylight and localtm.tm_isdst > 0
500
+ gmtoff = -(time.altzone if dst else time.timezone)
501
+ if delta == datetime.timedelta(seconds=gmtoff):
502
+ tz = datetime.timezone(delta, time.tzname[dst])
503
+ else:
504
+ tz = datetime.timezone(delta)
505
+ return dt.replace(tzinfo=tz)