singlestoredb 0.3.3__py3-none-any.whl → 1.0.3__py3-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 singlestoredb might be problematic. Click here for more details.

Files changed (121) hide show
  1. singlestoredb/__init__.py +33 -2
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +6 -4
  4. singlestoredb/config.py +116 -16
  5. singlestoredb/connection.py +489 -523
  6. singlestoredb/converters.py +275 -26
  7. singlestoredb/exceptions.py +30 -4
  8. singlestoredb/functions/__init__.py +1 -0
  9. singlestoredb/functions/decorator.py +142 -0
  10. singlestoredb/functions/dtypes.py +1639 -0
  11. singlestoredb/functions/ext/__init__.py +2 -0
  12. singlestoredb/functions/ext/arrow.py +375 -0
  13. singlestoredb/functions/ext/asgi.py +661 -0
  14. singlestoredb/functions/ext/json.py +427 -0
  15. singlestoredb/functions/ext/mmap.py +306 -0
  16. singlestoredb/functions/ext/rowdat_1.py +744 -0
  17. singlestoredb/functions/signature.py +673 -0
  18. singlestoredb/fusion/__init__.py +11 -0
  19. singlestoredb/fusion/graphql.py +213 -0
  20. singlestoredb/fusion/handler.py +621 -0
  21. singlestoredb/fusion/handlers/__init__.py +0 -0
  22. singlestoredb/fusion/handlers/stage.py +257 -0
  23. singlestoredb/fusion/handlers/utils.py +162 -0
  24. singlestoredb/fusion/handlers/workspace.py +412 -0
  25. singlestoredb/fusion/registry.py +164 -0
  26. singlestoredb/fusion/result.py +399 -0
  27. singlestoredb/http/__init__.py +27 -0
  28. singlestoredb/http/connection.py +1192 -0
  29. singlestoredb/management/__init__.py +3 -2
  30. singlestoredb/management/billing_usage.py +148 -0
  31. singlestoredb/management/cluster.py +19 -14
  32. singlestoredb/management/manager.py +100 -40
  33. singlestoredb/management/organization.py +188 -0
  34. singlestoredb/management/region.py +6 -8
  35. singlestoredb/management/utils.py +253 -4
  36. singlestoredb/management/workspace.py +1153 -35
  37. singlestoredb/mysql/__init__.py +177 -0
  38. singlestoredb/mysql/_auth.py +298 -0
  39. singlestoredb/mysql/charset.py +214 -0
  40. singlestoredb/mysql/connection.py +1814 -0
  41. singlestoredb/mysql/constants/CLIENT.py +38 -0
  42. singlestoredb/mysql/constants/COMMAND.py +32 -0
  43. singlestoredb/mysql/constants/CR.py +78 -0
  44. singlestoredb/mysql/constants/ER.py +474 -0
  45. singlestoredb/mysql/constants/FIELD_TYPE.py +32 -0
  46. singlestoredb/mysql/constants/FLAG.py +15 -0
  47. singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
  48. singlestoredb/mysql/constants/__init__.py +0 -0
  49. singlestoredb/mysql/converters.py +271 -0
  50. singlestoredb/mysql/cursors.py +713 -0
  51. singlestoredb/mysql/err.py +92 -0
  52. singlestoredb/mysql/optionfile.py +20 -0
  53. singlestoredb/mysql/protocol.py +388 -0
  54. singlestoredb/mysql/tests/__init__.py +19 -0
  55. singlestoredb/mysql/tests/base.py +126 -0
  56. singlestoredb/mysql/tests/conftest.py +37 -0
  57. singlestoredb/mysql/tests/test_DictCursor.py +132 -0
  58. singlestoredb/mysql/tests/test_SSCursor.py +141 -0
  59. singlestoredb/mysql/tests/test_basic.py +452 -0
  60. singlestoredb/mysql/tests/test_connection.py +851 -0
  61. singlestoredb/mysql/tests/test_converters.py +58 -0
  62. singlestoredb/mysql/tests/test_cursor.py +141 -0
  63. singlestoredb/mysql/tests/test_err.py +16 -0
  64. singlestoredb/mysql/tests/test_issues.py +514 -0
  65. singlestoredb/mysql/tests/test_load_local.py +75 -0
  66. singlestoredb/mysql/tests/test_nextset.py +88 -0
  67. singlestoredb/mysql/tests/test_optionfile.py +27 -0
  68. singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
  69. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  70. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
  71. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
  72. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
  73. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
  74. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
  75. singlestoredb/mysql/times.py +23 -0
  76. singlestoredb/pytest.py +283 -0
  77. singlestoredb/tests/empty.sql +0 -0
  78. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  79. singlestoredb/tests/test.sql +210 -0
  80. singlestoredb/tests/test2.sql +1 -0
  81. singlestoredb/tests/test_basics.py +482 -117
  82. singlestoredb/tests/test_config.py +13 -15
  83. singlestoredb/tests/test_connection.py +241 -289
  84. singlestoredb/tests/test_dbapi.py +27 -0
  85. singlestoredb/tests/test_exceptions.py +0 -2
  86. singlestoredb/tests/test_ext_func.py +1193 -0
  87. singlestoredb/tests/test_ext_func_data.py +1101 -0
  88. singlestoredb/tests/test_fusion.py +465 -0
  89. singlestoredb/tests/test_http.py +32 -28
  90. singlestoredb/tests/test_management.py +588 -10
  91. singlestoredb/tests/test_plugin.py +33 -0
  92. singlestoredb/tests/test_results.py +11 -14
  93. singlestoredb/tests/test_types.py +0 -2
  94. singlestoredb/tests/test_udf.py +687 -0
  95. singlestoredb/tests/test_xdict.py +0 -2
  96. singlestoredb/tests/utils.py +3 -4
  97. singlestoredb/types.py +4 -5
  98. singlestoredb/utils/config.py +71 -12
  99. singlestoredb/utils/convert_rows.py +0 -2
  100. singlestoredb/utils/debug.py +13 -0
  101. singlestoredb/utils/mogrify.py +151 -0
  102. singlestoredb/utils/results.py +4 -3
  103. singlestoredb/utils/xdict.py +12 -12
  104. singlestoredb-1.0.3.dist-info/METADATA +139 -0
  105. singlestoredb-1.0.3.dist-info/RECORD +112 -0
  106. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/WHEEL +1 -1
  107. singlestoredb-1.0.3.dist-info/entry_points.txt +2 -0
  108. singlestoredb/drivers/__init__.py +0 -46
  109. singlestoredb/drivers/base.py +0 -200
  110. singlestoredb/drivers/cymysql.py +0 -40
  111. singlestoredb/drivers/http.py +0 -49
  112. singlestoredb/drivers/mariadb.py +0 -42
  113. singlestoredb/drivers/mysqlconnector.py +0 -51
  114. singlestoredb/drivers/mysqldb.py +0 -62
  115. singlestoredb/drivers/pymysql.py +0 -39
  116. singlestoredb/drivers/pyodbc.py +0 -67
  117. singlestoredb/http.py +0 -794
  118. singlestoredb-0.3.3.dist-info/METADATA +0 -105
  119. singlestoredb-0.3.3.dist-info/RECORD +0 -46
  120. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/LICENSE +0 -0
  121. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,110 @@
1
+ # type: ignore
2
+ import warnings
3
+
4
+ import singlestoredb.mysql as sv
5
+ from . import capabilities
6
+ from singlestoredb.mysql.tests import base
7
+
8
+ warnings.filterwarnings('error')
9
+
10
+
11
+ class test_MySQLdb(capabilities.DatabaseTest):
12
+
13
+ db_module = sv
14
+ connect_args = ()
15
+ connect_kwargs = base.PyMySQLTestCase.databases[0].copy()
16
+ connect_kwargs.update(
17
+ dict(
18
+ read_default_file='~/.my.cnf',
19
+ use_unicode=True,
20
+ binary_prefix=True,
21
+ charset='utf8mb4',
22
+ sql_mode='ANSI,STRICT_TRANS_TABLES,TRADITIONAL',
23
+ ),
24
+ )
25
+
26
+ leak_test = False
27
+
28
+ def quote_identifier(self, ident):
29
+ return '`%s`' % ident
30
+
31
+ def test_TIME(self):
32
+ from datetime import timedelta
33
+
34
+ def generator(row, col):
35
+ return timedelta(0, row * 8000)
36
+
37
+ self.check_data_integrity(('col1 TIME',), generator)
38
+
39
+ def test_TINYINT(self):
40
+ # Number data
41
+ def generator(row, col):
42
+ v = (row * row) % 256
43
+ if v > 127:
44
+ v = v - 256
45
+ return v
46
+
47
+ self.check_data_integrity(('col1 TINYINT',), generator)
48
+
49
+ def test_stored_procedures(self):
50
+ db = self.connection
51
+ c = self.cursor
52
+ try:
53
+ self.create_table(('pos INT', 'tree CHAR(20)'))
54
+ c.executemany(
55
+ 'INSERT INTO %s (pos,tree) VALUES (%%s,%%s)' % self.table,
56
+ list(enumerate('ash birch cedar larch pine'.split())),
57
+ )
58
+ db.commit()
59
+
60
+ c.execute(
61
+ """
62
+ CREATE PROCEDURE test_sp(t VARCHAR(255)) AS
63
+ BEGIN
64
+ ECHO SELECT pos FROM %s WHERE tree = t;
65
+ END
66
+ """
67
+ % self.table,
68
+ )
69
+ db.commit()
70
+
71
+ c.callproc('test_sp', ('larch',))
72
+ rows = c.fetchall()
73
+ self.assertEqual(len(rows), 1)
74
+ self.assertEqual(rows[0][0], 3)
75
+ c.nextset()
76
+ finally:
77
+ c.execute('DROP PROCEDURE IF EXISTS test_sp')
78
+ c.execute('drop table %s' % (self.table))
79
+
80
+ def test_small_CHAR(self):
81
+ # Character data
82
+ def generator(row, col):
83
+ i = ((row + 1) * (col + 1) + 62) % 256
84
+ if i == 62:
85
+ return ''
86
+ if i == 63:
87
+ return None
88
+ return chr(i)
89
+
90
+ self.check_data_integrity(('col1 char(1)', 'col2 char(1)'), generator)
91
+
92
+ def test_bug_2671682(self):
93
+ from singlestoredb.mysql.constants import ER
94
+
95
+ try:
96
+ self.cursor.execute('describe some_non_existent_table')
97
+ except self.connection.ProgrammingError as msg:
98
+ self.assertEqual(msg.args[0], ER.NO_SUCH_TABLE)
99
+
100
+ def test_ping(self):
101
+ self.connection.ping()
102
+
103
+ def test_literal_int(self):
104
+ self.assertTrue('2' == self.connection.literal(2))
105
+
106
+ def test_literal_float(self):
107
+ self.assertEqual('3.1415e0', self.connection.literal(3.1415))
108
+
109
+ def test_literal_string(self):
110
+ self.assertTrue("'foo'" == self.connection.literal('foo'))
@@ -0,0 +1,224 @@
1
+ # type: ignore
2
+ import singlestoredb.mysql as sv
3
+ from . import dbapi20
4
+ from singlestoredb.mysql.tests import base
5
+
6
+
7
+ class test_MySQLdb(dbapi20.DatabaseAPI20Test):
8
+
9
+ driver = sv
10
+ connect_args = ()
11
+ connect_kw_args = base.PyMySQLTestCase.databases[0].copy()
12
+ connect_kw_args.update(
13
+ dict(
14
+ read_default_file='~/.my.cnf',
15
+ charset='utf8',
16
+ sql_mode='ANSI,STRICT_TRANS_TABLES,TRADITIONAL',
17
+ ),
18
+ )
19
+
20
+ def test_setoutputsize(self):
21
+ pass
22
+
23
+ def test_setoutputsize_basic(self):
24
+ pass
25
+
26
+ # The tests on fetchone and fetchall and rowcount bogusly
27
+ # test for an exception if the statement cannot return a
28
+ # result set. MySQL always returns a result set; it's just that
29
+ # some things return empty result sets.
30
+
31
+ def test_fetchall(self):
32
+ con = self._connect()
33
+ try:
34
+ cur = con.cursor()
35
+ # cursor.fetchall should raise an Error if called
36
+ # without executing a query that may return rows (such
37
+ # as a select)
38
+ self.assertRaises(self.driver.Error, cur.fetchall)
39
+
40
+ self.executeDDL1(cur)
41
+ for sql in self._populate():
42
+ cur.execute(sql)
43
+
44
+ # cursor.fetchall should raise an Error if called
45
+ # after executing a a statement that cannot return rows
46
+ # self.assertRaises(self.driver.Error,cur.fetchall)
47
+
48
+ cur.execute('select name from %sbooze' % self.table_prefix)
49
+ rows = cur.fetchall()
50
+ self.assertTrue(cur.rowcount in (-1, len(self.samples)))
51
+ self.assertEqual(
52
+ len(rows),
53
+ len(self.samples),
54
+ 'cursor.fetchall did not retrieve all rows',
55
+ )
56
+ rows = [r[0] for r in rows]
57
+ rows.sort()
58
+ for i in range(0, len(self.samples)):
59
+ self.assertEqual(
60
+ rows[i], self.samples[i], 'cursor.fetchall retrieved incorrect rows',
61
+ )
62
+ rows = cur.fetchall()
63
+ self.assertEqual(
64
+ len(rows),
65
+ 0,
66
+ 'cursor.fetchall should return an empty list if called '
67
+ 'after the whole result set has been fetched',
68
+ )
69
+ self.assertTrue(cur.rowcount in (-1, len(self.samples)))
70
+
71
+ self.executeDDL2(cur)
72
+ cur.execute('select name from %sbarflys' % self.table_prefix)
73
+ rows = cur.fetchall()
74
+ self.assertTrue(cur.rowcount in (-1, 0))
75
+ self.assertEqual(
76
+ len(rows),
77
+ 0,
78
+ 'cursor.fetchall should return an empty list if '
79
+ 'a select query returns no rows',
80
+ )
81
+
82
+ finally:
83
+ con.close()
84
+
85
+ def test_fetchone(self):
86
+ con = self._connect()
87
+ try:
88
+ cur = con.cursor()
89
+
90
+ # cursor.fetchone should raise an Error if called before
91
+ # executing a select-type query
92
+ self.assertRaises(self.driver.Error, cur.fetchone)
93
+
94
+ # cursor.fetchone should raise an Error if called after
95
+ # executing a query that cannnot return rows
96
+ self.executeDDL1(cur)
97
+ # self.assertRaises(self.driver.Error,cur.fetchone)
98
+
99
+ cur.execute('select name from %sbooze' % self.table_prefix)
100
+ self.assertEqual(
101
+ cur.fetchone(),
102
+ None,
103
+ 'cursor.fetchone should return None if a query retrieves ' 'no rows',
104
+ )
105
+ self.assertTrue(cur.rowcount in (-1, 0))
106
+
107
+ # cursor.fetchone should raise an Error if called after
108
+ # executing a query that cannnot return rows
109
+ cur.execute(
110
+ "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix),
111
+ )
112
+ # self.assertRaises(self.driver.Error,cur.fetchone)
113
+
114
+ cur.execute('select name from %sbooze' % self.table_prefix)
115
+ r = cur.fetchone()
116
+ self.assertEqual(
117
+ len(r), 1, 'cursor.fetchone should have retrieved a single row',
118
+ )
119
+ self.assertEqual(
120
+ r[0], 'Victoria Bitter', 'cursor.fetchone retrieved incorrect data',
121
+ )
122
+ # self.assertEqual(cur.fetchone(), None,
123
+ # 'cursor.fetchone should return None if '
124
+ # 'no more rows available'
125
+ # )
126
+ self.assertTrue(cur.rowcount in (-1, 1))
127
+ finally:
128
+ con.close()
129
+
130
+ # Same complaint as for fetchall and fetchone
131
+ def test_rowcount(self):
132
+ con = self._connect()
133
+ try:
134
+ cur = con.cursor()
135
+ self.executeDDL1(cur)
136
+ # self.assertEqual(cur.rowcount,-1,
137
+ # 'cursor.rowcount should be -1 after executing no-result '
138
+ # 'statements'
139
+ # )
140
+ cur.execute(
141
+ "insert into %sbooze values ('Victoria Bitter')" % (self.table_prefix),
142
+ )
143
+ # self.assertTrue(cur.rowcount in (-1,1),
144
+ # 'cursor.rowcount should == number or rows inserted, or '
145
+ # 'set to -1 after executing an insert statement'
146
+ # )
147
+ cur.execute('select name from %sbooze' % self.table_prefix)
148
+ self.assertTrue(
149
+ cur.rowcount in (-1, 1),
150
+ 'cursor.rowcount should == number of rows returned, or '
151
+ 'set to -1 after executing a select statement',
152
+ )
153
+ cur.fetchall()
154
+ self.executeDDL2(cur)
155
+ # self.assertEqual(cur.rowcount, -1,
156
+ # 'cursor.rowcount not being reset to -1 after executing '
157
+ # 'no-result statements'
158
+ # )
159
+ finally:
160
+ con.close()
161
+
162
+ def test_callproc(self):
163
+ pass # performed in test_MySQL_capabilities
164
+
165
+ def help_nextset_setUp(self, cur):
166
+ """Should create a procedure called deleteme
167
+ that returns two result sets, first the
168
+ number of rows in booze then "name from booze"
169
+ """
170
+ sql = """
171
+ create procedure deleteme() as
172
+ begin
173
+ echo select count(*) from %(tp)sbooze;
174
+ echo select name from %(tp)sbooze;
175
+ end
176
+ """ % dict(
177
+ tp=self.table_prefix,
178
+ )
179
+ cur.execute(sql)
180
+
181
+ def help_nextset_tearDown(self, cur):
182
+ # If cleaning up is needed after nextSetTest
183
+ cur.execute('drop procedure deleteme')
184
+
185
+ def test_nextset(self):
186
+ # from warnings import warn
187
+
188
+ con = self._connect()
189
+ try:
190
+ cur = con.cursor()
191
+ if not hasattr(cur, 'nextset'):
192
+ return
193
+
194
+ try:
195
+ self.executeDDL1(cur)
196
+ sql = self._populate()
197
+ for sql in self._populate():
198
+ cur.execute(sql)
199
+
200
+ self.help_nextset_setUp(cur)
201
+
202
+ cur.callproc('deleteme')
203
+ numberofrows = cur.fetchone()
204
+ assert numberofrows[0] == len(self.samples)
205
+ if cur._result.unbuffered_active:
206
+ assert len(cur.fetchall()) == 0
207
+ assert cur.nextset()
208
+ names = cur.fetchall()
209
+ assert len(names) == len(self.samples)
210
+ s = cur.nextset()
211
+ if s:
212
+ empty = cur.fetchall()
213
+ self.assertEqual(
214
+ len(empty), 0, 'non-empty result set after other result sets',
215
+ )
216
+ # warn("Incompatibility: MySQL returns an empty result set "
217
+ # "for the CALL itself",
218
+ # Warning)
219
+ # assert s == None,'No more return sets, should return None'
220
+ finally:
221
+ self.help_nextset_tearDown(cur)
222
+
223
+ finally:
224
+ con.close()
@@ -0,0 +1,101 @@
1
+ # type: ignore
2
+ import unittest
3
+
4
+ import singlestoredb.mysql as sv
5
+ from singlestoredb.mysql.constants import FIELD_TYPE
6
+ from singlestoredb.mysql.tests import base
7
+
8
+ _mysql = sv
9
+
10
+
11
+ class TestDBAPISet(unittest.TestCase):
12
+
13
+ def test_set_equality(self):
14
+ self.assertTrue(sv.STRING == sv.STRING)
15
+
16
+ def test_set_inequality(self):
17
+ self.assertTrue(sv.STRING != sv.NUMBER)
18
+
19
+ def test_set_equality_membership(self):
20
+ self.assertTrue(FIELD_TYPE.VAR_STRING == sv.STRING)
21
+
22
+ def test_set_inequality_membership(self):
23
+ self.assertTrue(FIELD_TYPE.DATE != sv.STRING)
24
+
25
+
26
+ class CoreModule(unittest.TestCase):
27
+ """Core _mysql module features."""
28
+
29
+ def test_NULL(self):
30
+ """Should have a NULL constant."""
31
+ self.assertEqual(_mysql.NULL, 'NULL')
32
+
33
+ def test_version(self):
34
+ """Version information sanity."""
35
+ self.assertTrue(isinstance(_mysql.__version__, str))
36
+
37
+ self.assertTrue(isinstance(_mysql.version_info, tuple))
38
+ self.assertEqual(len(_mysql.version_info), 5)
39
+
40
+ def test_client_info(self):
41
+ self.assertTrue(isinstance(_mysql.get_client_info(), str))
42
+
43
+ def test_thread_safe(self):
44
+ self.assertTrue(isinstance(_mysql.thread_safe(), int))
45
+
46
+
47
+ class CoreAPI(unittest.TestCase):
48
+ """Test _mysql interaction internals."""
49
+
50
+ def setUp(self):
51
+ kwargs = base.PyMySQLTestCase.databases[0].copy()
52
+ kwargs['read_default_file'] = '~/.my.cnf'
53
+ self.conn = _mysql.connect(**kwargs)
54
+
55
+ def tearDown(self):
56
+ self.conn.close()
57
+
58
+ def test_thread_id(self):
59
+ tid = self.conn.thread_id()
60
+ self.assertTrue(
61
+ isinstance(tid, int), "thread_id didn't return an integral value.",
62
+ )
63
+
64
+ self.assertRaises(
65
+ TypeError,
66
+ self.conn.thread_id,
67
+ ('evil',),
68
+ "thread_id shouldn't accept arguments.",
69
+ )
70
+
71
+ def test_affected_rows(self):
72
+ self.assertEqual(
73
+ self.conn.affected_rows(), 0, 'Should return 0 before we do anything.',
74
+ )
75
+
76
+ # def test_debug(self):
77
+ # FIXME Only actually tests if you lack SUPER
78
+ # self.assertRaises(sv.OperationalError,
79
+ # self.conn.dump_debug_info)
80
+
81
+ def test_charset_name(self):
82
+ self.assertTrue(
83
+ isinstance(self.conn.character_set_name(), str), 'Should return a string.',
84
+ )
85
+
86
+ def test_host_info(self):
87
+ assert isinstance(self.conn.get_host_info(), str), 'should return a string'
88
+
89
+ def test_proto_info(self):
90
+ self.assertTrue(
91
+ isinstance(self.conn.get_proto_info(), int), 'Should return an int.',
92
+ )
93
+
94
+ def test_server_info(self):
95
+ self.assertTrue(
96
+ isinstance(self.conn.get_server_info(), str), 'Should return an str.',
97
+ )
98
+
99
+
100
+ if __name__ == '__main__':
101
+ unittest.main()
@@ -0,0 +1,23 @@
1
+ from datetime import date
2
+ from datetime import datetime
3
+ from datetime import time
4
+ from datetime import timedelta
5
+ from time import localtime
6
+
7
+
8
+ Date = date
9
+ Time = time
10
+ TimeDelta = timedelta
11
+ Timestamp = datetime
12
+
13
+
14
+ def DateFromTicks(ticks: int) -> date:
15
+ return date(*localtime(ticks)[:3])
16
+
17
+
18
+ def TimeFromTicks(ticks: int) -> time:
19
+ return time(*localtime(ticks)[3:6])
20
+
21
+
22
+ def TimestampFromTicks(ticks: int) -> datetime:
23
+ return datetime(*localtime(ticks)[:6])