arrowsql 0.1.0__cp313-cp313-win_amd64.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.
Files changed (45) hide show
  1. arrowsql/__init__.py +653 -0
  2. arrowsql/_rust_auth.py +170 -0
  3. arrowsql/_server_sqlstate.py +11 -0
  4. arrowsql/_utf8_strings.py +78 -0
  5. arrowsql/agent.py +620 -0
  6. arrowsql/aio.py +533 -0
  7. arrowsql/arrow_parallel.py +464 -0
  8. arrowsql/async_query/__init__.py +40 -0
  9. arrowsql/async_query/_connection_context.py +39 -0
  10. arrowsql/async_query/_native.py +24 -0
  11. arrowsql/async_query/async_connection.py +160 -0
  12. arrowsql/async_query/async_cursor.py +225 -0
  13. arrowsql/async_query/async_execute.py +113 -0
  14. arrowsql/async_query/async_fetch.py +153 -0
  15. arrowsql/async_query/exception_translator.py +132 -0
  16. arrowsql/auth.py +913 -0
  17. arrowsql/bulk_parallel.py +385 -0
  18. arrowsql/connection.py +2377 -0
  19. arrowsql/connection_string_builder.py +114 -0
  20. arrowsql/connection_string_parser.py +417 -0
  21. arrowsql/constants.py +701 -0
  22. arrowsql/cursor.py +4516 -0
  23. arrowsql/db_connection.py +98 -0
  24. arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
  25. arrowsql/ddbc_bindings.pyi +283 -0
  26. arrowsql/decimal_config.py +98 -0
  27. arrowsql/exceptions.py +378 -0
  28. arrowsql/helpers.py +436 -0
  29. arrowsql/logging.py +700 -0
  30. arrowsql/mssql_python.pyi +579 -0
  31. arrowsql/odbc_provider.py +269 -0
  32. arrowsql/parameter_helper.py +442 -0
  33. arrowsql/perf_timer.py +286 -0
  34. arrowsql/pooling.py +147 -0
  35. arrowsql/py.typed +1 -0
  36. arrowsql/row.py +462 -0
  37. arrowsql/type.py +160 -0
  38. arrowsql-0.1.0.dist-info/METADATA +146 -0
  39. arrowsql-0.1.0.dist-info/RECORD +45 -0
  40. arrowsql-0.1.0.dist-info/WHEEL +4 -0
  41. arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
  42. arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
  44. arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
  45. mssql_python/__init__.py +39 -0
arrowsql/row.py ADDED
@@ -0,0 +1,462 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+ This module contains the Row class, which represents a single row of data
5
+ from a cursor fetch operation.
6
+ """
7
+
8
+ import decimal
9
+ import uuid as _uuid
10
+ from collections.abc import Mapping
11
+ from typing import Any
12
+ from arrowsql.logging import logger
13
+
14
+
15
+ class Row:
16
+ """
17
+ A row of data from a cursor fetch operation. Provides both tuple-like indexing
18
+ and attribute access to column values.
19
+
20
+ Rows support user-defined attributes, ``vars(row)``, and weak references.
21
+ Internal fields use slots; ``vars(row)`` contains user-defined attributes.
22
+
23
+ For dict-like access, use the read-only ``row._mapping`` view (a
24
+ ``collections.abc.Mapping`` of column name -> value). Iterating the Row itself
25
+ (for x in row) yields values, not keys — consistent with pyodbc.Row and
26
+ sqlite3.Row; iterate ``row._mapping`` to get column names.
27
+
28
+ Column attribute access behavior depends on the global 'lowercase' setting:
29
+ - When enabled: Case-insensitive attribute access
30
+ - When disabled (default): Case-sensitive attribute access matching original column names
31
+
32
+ Example:
33
+ row = cursor.fetchone()
34
+ print(row[0]) # Access by index
35
+ print(row.column_name) # Access by column name
36
+ print(dict(row._mapping)) # Convert to a plain dict
37
+ print(row._mapping["col"]) # Access a value by column name via the mapping
38
+ for name in row._mapping: # Iterate column names
39
+ print(name, row._mapping[name])
40
+ for value in row: # Iterating the Row yields values, not keys
41
+ print(value)
42
+ """
43
+
44
+ # Slot internal fields while preserving dynamic attributes and weak references.
45
+ __slots__ = (
46
+ "_row_values",
47
+ "_column_map",
48
+ "_row_cursor",
49
+ "_column_map_lower",
50
+ "_column_names",
51
+ "__dict__",
52
+ "__weakref__",
53
+ )
54
+
55
+ @staticmethod
56
+ def _fast_create(values, column_map, cursor, column_map_lower=None, column_names=None):
57
+ """Construct a Row bypassing __init__ — for the common fast path.
58
+
59
+ Used by fetchall/fetchmany when no output converters and no UUID
60
+ stringification are needed (the vast majority of queries). Skips
61
+ the entire if/elif/else chain and keyword argument overhead in __init__.
62
+ """
63
+ r = Row.__new__(Row)
64
+ r._row_values = values
65
+ r._column_map = column_map
66
+ r._row_cursor = cursor
67
+ r._column_map_lower = column_map_lower
68
+ r._column_names = column_names
69
+ return r
70
+
71
+ @staticmethod
72
+ def _converted_create(
73
+ values, pairs, column_map, cursor, column_map_lower, column_names, uuid_idx
74
+ ):
75
+ """Construct a Row bypassing __init__ when output converters apply.
76
+
77
+ ``pairs`` is ``((column index, converter), ...)`` for the converted columns
78
+ only. Same semantics as ``_apply_output_converters_optimized``: None is
79
+ skipped, str is passed as UTF-16-LE bytes, a failing converter keeps the
80
+ original value. The values are copied once.
81
+ """
82
+ converted = list(values)
83
+ for i, converter in pairs:
84
+ value = values[i]
85
+ if value is None:
86
+ continue
87
+ try:
88
+ if isinstance(value, str):
89
+ converted[i] = converter(value.encode("utf-16-le"))
90
+ else:
91
+ converted[i] = converter(value)
92
+ except Exception:
93
+ pass
94
+ r = Row.__new__(Row)
95
+ r._row_values = converted
96
+ r._column_map = column_map
97
+ r._row_cursor = cursor
98
+ r._column_map_lower = column_map_lower
99
+ r._column_names = column_names
100
+ if uuid_idx:
101
+ r._stringify_uuids(uuid_idx)
102
+ return r
103
+
104
+ def __init__(
105
+ self,
106
+ values,
107
+ column_map,
108
+ cursor=None,
109
+ converter_map=None,
110
+ uuid_str_indices=None,
111
+ column_map_lower=None,
112
+ column_names=None,
113
+ ):
114
+ """
115
+ Initialize a Row object with values and pre-built column map.
116
+ Args:
117
+ values: List of values for this row
118
+ column_map: Pre-built column name to index mapping (shared across rows)
119
+ cursor: Optional cursor reference (for backward compatibility and lowercase access)
120
+ converter_map: Pre-computed converter map (shared across rows for performance).
121
+ An empty sequence skips converters; None enables the connection fallback.
122
+ uuid_str_indices: Tuple of column indices whose uuid.UUID values should be
123
+ converted to str. Pre-computed once per result set when native_uuid=False.
124
+ None means no conversion (native_uuid=True, the default).
125
+ column_map_lower: Pre-built lowercase column map for O(1) case-insensitive
126
+ lookups. Built once per result set in the cursor when lowercase is enabled;
127
+ None when lowercase is off (the default). Shared across all rows.
128
+ column_names: Canonical, order- and duplicate-preserving column names for
129
+ the result set, snapshotted once by the cursor and shared by reference
130
+ across all rows. Backs ``row._mapping``. None for rows built without a
131
+ cursor snapshot; ``_mapping_keys()`` then reconstructs names from
132
+ ``column_map``.
133
+ """
134
+ if converter_map:
135
+ self._row_values = self._apply_output_converters_optimized(values, converter_map)
136
+ elif (
137
+ converter_map is None
138
+ and cursor
139
+ and hasattr(cursor.connection, "_output_converters")
140
+ and cursor.connection._output_converters
141
+ ):
142
+ # Support direct Row construction without a pre-computed converter map.
143
+ self._row_values = self._apply_output_converters(values, cursor)
144
+ else:
145
+ self._row_values = values
146
+
147
+ if uuid_str_indices:
148
+ self._stringify_uuids(uuid_str_indices)
149
+
150
+ self._column_map = column_map
151
+ self._row_cursor = cursor
152
+ # Lowercase map is pre-built once per result set in the cursor and shared
153
+ # across all rows. None when lowercase is off (the default) — zero cost.
154
+ self._column_map_lower = column_map_lower
155
+ # Canonical column names for this row's result set, snapshotted once by the
156
+ # cursor (order- and duplicate-preserving) and shared by reference across every
157
+ # row. None only for rows built without a cursor snapshot (e.g. some direct or
158
+ # test constructions); _mapping_keys() then reconstructs names from _column_map.
159
+ self._column_names = column_names
160
+
161
+ def _stringify_uuids(self, indices):
162
+ """
163
+ Convert uuid.UUID values at the given column indices to uppercase str in-place.
164
+
165
+ This is only called when native_uuid=False. It operates directly on
166
+ self._row_values to avoid creating an extra list copy.
167
+ """
168
+ vals = self._row_values
169
+ # If values are still the original list (no converters), we need a mutable copy
170
+ if not isinstance(vals, list):
171
+ vals = list(vals)
172
+ self._row_values = vals
173
+
174
+ for i in indices:
175
+ v = vals[i]
176
+ if v is not None and isinstance(v, _uuid.UUID):
177
+ vals[i] = str(v).upper()
178
+
179
+ def _apply_output_converters(self, values, cursor):
180
+ """
181
+ Apply output converters to raw values.
182
+
183
+ Args:
184
+ values: Raw values from the database
185
+ cursor: Cursor object with connection and description
186
+
187
+ Returns:
188
+ List of converted values
189
+ """
190
+ if not cursor.description:
191
+ return values
192
+
193
+ converted_values = list(values)
194
+
195
+ for i, (value, desc) in enumerate(zip(values, cursor.description)):
196
+ if desc is None or value is None:
197
+ continue
198
+
199
+ # Get SQL type from description
200
+ sql_type = desc[1] # type_code is at index 1 in description tuple
201
+
202
+ # Try to get a converter for this type
203
+ converter = cursor.connection.get_output_converter(sql_type)
204
+
205
+ # If no converter found for the SQL type but the value is a string or bytes,
206
+ # try the WVARCHAR converter as a fallback
207
+ if converter is None and isinstance(value, (str, bytes)):
208
+ from arrowsql.constants import ConstantsDDBC
209
+
210
+ converter = cursor.connection.get_output_converter(ConstantsDDBC.SQL_WVARCHAR.value)
211
+
212
+ # If we found a converter, apply it
213
+ if converter:
214
+ try:
215
+ # If value is already a Python type (str, int, etc.),
216
+ # we need to convert it to bytes for our converters
217
+ if isinstance(value, str):
218
+ # Encode as UTF-16LE for string values (SQL_WVARCHAR format)
219
+ value_bytes = value.encode("utf-16-le")
220
+ converted_values[i] = converter(value_bytes)
221
+ else:
222
+ converted_values[i] = converter(value)
223
+ except Exception:
224
+ logger.debug("Exception occurred in output converter", exc_info=True)
225
+ # If conversion fails, keep the original value
226
+ pass
227
+
228
+ return converted_values
229
+
230
+ def _apply_output_converters_optimized(self, values, converter_map):
231
+ """
232
+ Apply output converters using pre-computed converter map for optimal performance.
233
+
234
+ Args:
235
+ values: Raw values from the database
236
+ converter_map: Pre-computed list of converters (one per column, None if no converter)
237
+
238
+ Returns:
239
+ List of converted values
240
+ """
241
+ converted_values = list(values)
242
+
243
+ for i, (value, converter) in enumerate(zip(values, converter_map)):
244
+ if converter and value is not None:
245
+ try:
246
+ if isinstance(value, str):
247
+ value_bytes = value.encode("utf-16-le")
248
+ converted_values[i] = converter(value_bytes)
249
+ else:
250
+ converted_values[i] = converter(value)
251
+ except Exception:
252
+ pass
253
+
254
+ return converted_values
255
+
256
+ def __getitem__(self, index) -> Any:
257
+ """Allow accessing by numeric index (row[0]) or column name (row["col"])."""
258
+ if type(index) is int:
259
+ return self._row_values[index]
260
+ if isinstance(index, str):
261
+ # A row built without a column map has no named columns, so any
262
+ # string key is simply absent (KeyError), never a TypeError.
263
+ if self._column_map is not None:
264
+ idx = self._column_map.get(index)
265
+ if idx is not None:
266
+ return self._row_values[idx]
267
+ # O(1) case-insensitive lookup when lowercase is enabled
268
+ if self._column_map_lower is not None:
269
+ idx = self._column_map_lower.get(index.lower())
270
+ if idx is not None:
271
+ return self._row_values[idx]
272
+ raise KeyError(f"Row has no column '{index}'")
273
+ if isinstance(index, (int, slice)):
274
+ return self._row_values[index]
275
+ raise TypeError(
276
+ f"Row indices must be integers, slices, or strings, not {type(index).__name__}"
277
+ )
278
+
279
+ def __getattr__(self, name: str) -> Any:
280
+ """
281
+ Allow accessing by column name as attribute: row.column_name
282
+
283
+ Note: Case sensitivity depends on the global 'lowercase' setting:
284
+ - When lowercase=True: Column names are stored in lowercase, enabling
285
+ case-insensitive attribute access (e.g., row.NAME, row.name, row.Name all work).
286
+ - When lowercase=False (default): Column names preserve original casing,
287
+ requiring exact case matching for attribute access.
288
+ """
289
+ # Handle lowercase attribute access - if lowercase is enabled,
290
+ # try to match attribute names case-insensitively
291
+ idx = self._column_map.get(name)
292
+ if idx is not None:
293
+ return self._row_values[idx]
294
+
295
+ # O(1) case-insensitive lookup when lowercase is enabled
296
+ if self._column_map_lower is not None:
297
+ idx = self._column_map_lower.get(name.lower())
298
+ if idx is not None:
299
+ return self._row_values[idx]
300
+
301
+ # RUST-PORT: the internal slots are named _row_values / _row_cursor so a
302
+ # column named _values or _cursor resolves to the column above. The old
303
+ # names stay readable here for callers that use them.
304
+ if name == "_values":
305
+ return self._row_values
306
+ if name == "_cursor":
307
+ return self._row_cursor
308
+
309
+ raise AttributeError(f"Row has no attribute '{name}'")
310
+
311
+ @property
312
+ def _mapping(self) -> "RowMapping":
313
+ """Read-only ``dict``-like view (column name -> value) over this row.
314
+
315
+ Returns a :class:`RowMapping` (a ``collections.abc.Mapping``). Typical use::
316
+
317
+ row = cursor.fetchone()
318
+ dict(row._mapping) # {'id': 1, 'name': 'Alice'}
319
+ for name, value in row._mapping.items():
320
+ ...
321
+ row._mapping["name"] # value by column name
322
+ "name" in row._mapping # membership by column name
323
+
324
+ Semantics and caveats:
325
+
326
+ - Keys are the result set's column names, order-preserving and
327
+ de-duplicated: when a name repeats, one key is kept and the last column
328
+ with that name supplies its value (matching ``row[name]`` / ``row.name``).
329
+ Every duplicate value stays reachable positionally via ``row[i]``.
330
+ - Lookup and membership use the canonical column names exactly, so the key
331
+ set, ``in`` and ``[]`` always agree. Unlike ``row[name]`` / ``row.name``,
332
+ the view does NOT resolve case-insensitive names or catalog aliases; with
333
+ ``lowercase=True`` the keys are the lowercased names.
334
+ - ``_mapping`` is a property, so a column literally named ``_mapping`` is
335
+ shadowed: read it with ``row["_mapping"]`` or ``row._mapping["_mapping"]``.
336
+ """
337
+ return RowMapping(self)
338
+
339
+ def _mapping_keys(self) -> tuple:
340
+ """Canonical, order-preserving column names backing ``_mapping``.
341
+
342
+ Prefers the names snapshotted once by the cursor for the result set, which
343
+ preserve the result set's column order. When a row was built without that
344
+ snapshot (e.g. a direct ``Row(values, column_map)`` construction), names are
345
+ reconstructed from ``_column_map`` in column-index order; that order can
346
+ differ from ``_column_map``'s insertion order, so a directly-constructed row
347
+ may key differently from an otherwise-equivalent cursor row. Returns ``()``
348
+ when neither source is available. Normal cursor fetches always supply the
349
+ snapshot.
350
+ """
351
+ if self._column_names is not None:
352
+ return self._column_names
353
+ if self._column_map:
354
+ idx_to_name: dict = {}
355
+ for name, idx in self._column_map.items():
356
+ idx_to_name.setdefault(idx, name)
357
+ return tuple(idx_to_name[i] for i in sorted(idx_to_name))
358
+ return ()
359
+
360
+ def __eq__(self, other: Any) -> bool:
361
+ """
362
+ Support comparison with lists for test compatibility.
363
+ This is the key change needed to fix the tests.
364
+ """
365
+ if isinstance(other, list):
366
+ return self._row_values == other
367
+ if isinstance(other, Row):
368
+ return self._row_values == other._row_values
369
+ return super().__eq__(other)
370
+
371
+ def __len__(self) -> int:
372
+ """Return the number of values in the row"""
373
+ return len(self._row_values)
374
+
375
+ def __iter__(self) -> Any:
376
+ """Allow iteration through values"""
377
+ return iter(self._row_values)
378
+
379
+ def __str__(self) -> str:
380
+ """Return string representation of the row"""
381
+ # Local import to avoid circular dependency
382
+ from arrowsql import getDecimalSeparator
383
+
384
+ parts = []
385
+ for value in self:
386
+ if isinstance(value, decimal.Decimal):
387
+ # Apply custom decimal separator for display
388
+ sep = getDecimalSeparator()
389
+ if sep != "." and value is not None:
390
+ s = str(value)
391
+ if "." in s:
392
+ s = s.replace(".", sep)
393
+ parts.append(s)
394
+ else:
395
+ parts.append(str(value))
396
+ else:
397
+ parts.append(repr(value))
398
+
399
+ return "(" + ", ".join(parts) + ")"
400
+
401
+ def __repr__(self) -> str:
402
+ """Return a detailed string representation for debugging"""
403
+ return repr(tuple(self._row_values))
404
+
405
+
406
+ # RowMapping key data per result set: ``id(names)`` -> (names, unique keys, key set).
407
+ # Every row of a result set shares one ``_column_names`` tuple, so the
408
+ # de-duplicated keys are computed once instead of on every lookup. The entry
409
+ # holds ``names`` itself, so an id can only match while that tuple is alive.
410
+ _MAPPING_KEYS_CACHE: dict = {}
411
+ _MAPPING_KEYS_CACHE_MAX = 64
412
+
413
+
414
+ def _mapping_key_entry(row: "Row"):
415
+ names = row._column_names
416
+ if names is None:
417
+ unique = tuple(dict.fromkeys(row._mapping_keys()))
418
+ return (None, unique, frozenset(unique))
419
+ entry = _MAPPING_KEYS_CACHE.get(id(names))
420
+ if entry is not None and entry[0] is names:
421
+ return entry
422
+ unique = tuple(dict.fromkeys(names))
423
+ entry = (names, unique, frozenset(unique))
424
+ if len(_MAPPING_KEYS_CACHE) >= _MAPPING_KEYS_CACHE_MAX:
425
+ _MAPPING_KEYS_CACHE.clear()
426
+ _MAPPING_KEYS_CACHE[id(names)] = entry
427
+ return entry
428
+
429
+
430
+ class RowMapping(Mapping):
431
+ """Read-only ``Mapping`` view over a :class:`Row` (column name -> value).
432
+
433
+ Created via :attr:`Row._mapping`. Keys are the row's canonical column names,
434
+ order-preserving and de-duplicated (last column wins for a repeated name).
435
+ Lookup and membership use those names exactly -- no case-insensitive or catalog
436
+ alias resolution -- so iteration, ``in`` and ``[]`` always agree. The view
437
+ reflects the row it wraps and copies no values.
438
+ """
439
+
440
+ __slots__ = ("_row",)
441
+
442
+ def __init__(self, row: "Row") -> None:
443
+ self._row = row
444
+
445
+ def __getitem__(self, key: str) -> Any:
446
+ # Restrict lookups to the canonical column names yielded by __iter__ so
447
+ # membership and lookup agree with iteration (proper Mapping semantics).
448
+ # Row.__getitem__ additionally accepts case-insensitive names and catalog
449
+ # aliases, but those are not iterated keys, so the view must not resolve
450
+ # them here.
451
+ if isinstance(key, str) and key in _mapping_key_entry(self._row)[2]:
452
+ return self._row[key]
453
+ raise KeyError(key)
454
+
455
+ def __iter__(self):
456
+ return iter(_mapping_key_entry(self._row)[1])
457
+
458
+ def __len__(self) -> int:
459
+ return len(_mapping_key_entry(self._row)[1])
460
+
461
+ def __repr__(self) -> str:
462
+ return f"RowMapping({dict(self)!r})"
arrowsql/type.py ADDED
@@ -0,0 +1,160 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+ This module contains type objects and constructors for the mssql_python package.
5
+ """
6
+
7
+ import datetime
8
+ import time
9
+ from typing import Union
10
+
11
+
12
+ # Type Objects
13
+ class STRING(str):
14
+ """
15
+ This type object is used to describe columns in a database that are string-based (e.g. CHAR).
16
+ """
17
+
18
+ def __new__(cls):
19
+ return str.__new__(cls, "")
20
+
21
+
22
+ class BINARY(bytearray):
23
+ """
24
+ This type object is used to describe (long)
25
+ binary columns in a database (e.g. LONG, RAW, BLOBs).
26
+ """
27
+
28
+ def __new__(cls):
29
+ return bytearray.__new__(cls)
30
+
31
+
32
+ class NUMBER(float):
33
+ """
34
+ This type object is used to describe numeric columns in a database.
35
+ """
36
+
37
+ def __new__(cls):
38
+ return float.__new__(cls, 0.0)
39
+
40
+
41
+ class DATETIME(datetime.datetime):
42
+ """
43
+ This type object is used to describe date/time columns in a database.
44
+ """
45
+
46
+ def __new__(
47
+ cls,
48
+ year: int = 1,
49
+ month: int = 1,
50
+ day: int = 1,
51
+ hour: int = 0,
52
+ minute: int = 0,
53
+ second: int = 0,
54
+ microsecond: int = 0,
55
+ tzinfo=None,
56
+ *,
57
+ fold: int = 0,
58
+ ):
59
+ return datetime.datetime.__new__(
60
+ cls, year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold
61
+ )
62
+
63
+
64
+ class ROWID(int):
65
+ """
66
+ This type object is used to describe the "Row ID" column in a database.
67
+ """
68
+
69
+ def __new__(cls):
70
+ return int.__new__(cls, 0)
71
+
72
+
73
+ # Type Constructors
74
+ def Date(year: int, month: int, day: int) -> datetime.date:
75
+ """
76
+ Generates a date object.
77
+ """
78
+ return datetime.date(year, month, day)
79
+
80
+
81
+ def Time(hour: int, minute: int, second: int) -> datetime.time:
82
+ """
83
+ Generates a time object.
84
+ """
85
+ return datetime.time(hour, minute, second)
86
+
87
+
88
+ def Timestamp(
89
+ year: int,
90
+ month: int,
91
+ day: int,
92
+ hour: int,
93
+ minute: int,
94
+ second: int,
95
+ microsecond: int = 0,
96
+ ) -> datetime.datetime:
97
+ """
98
+ Generates a timestamp object.
99
+ """
100
+ return datetime.datetime(year, month, day, hour, minute, second, microsecond)
101
+
102
+
103
+ def DateFromTicks(ticks: int) -> datetime.date:
104
+ """
105
+ Generates a date object from ticks.
106
+ """
107
+ return datetime.date.fromtimestamp(ticks)
108
+
109
+
110
+ def TimeFromTicks(ticks: int) -> datetime.time:
111
+ """
112
+ Generates a time object from ticks.
113
+ """
114
+ return datetime.time(*time.localtime(ticks)[3:6])
115
+
116
+
117
+ def TimestampFromTicks(ticks: int) -> datetime.datetime:
118
+ """
119
+ Generates a timestamp object from ticks.
120
+ """
121
+ return datetime.datetime.fromtimestamp(ticks)
122
+
123
+
124
+ def Binary(value: Union[str, bytes, bytearray, memoryview]) -> bytes:
125
+ """
126
+ Converts a string or bytes-like object to bytes for binary database columns.
127
+
128
+ This function follows the DB-API 2.0 specification. It accepts str,
129
+ bytes, bytearray, and memoryview. memoryview is accepted so buffer-protocol
130
+ values (for example the memoryview Django hands a BinaryField) round-trip the
131
+ same way pyodbc handles them.
132
+
133
+ Args:
134
+ value: A string (str) or bytes-like object (bytes, bytearray, memoryview)
135
+
136
+ Returns:
137
+ bytes: The input converted to bytes
138
+
139
+ Raises:
140
+ TypeError: If the input type is not supported
141
+
142
+ Examples:
143
+ Binary("hello") # Returns b"hello"
144
+ Binary(b"hello") # Returns b"hello"
145
+ Binary(bytearray(b"hi")) # Returns b"hi"
146
+ Binary(memoryview(b"hi")) # Returns b"hi"
147
+ """
148
+ if isinstance(value, bytes):
149
+ return value
150
+ if isinstance(value, bytearray):
151
+ return bytes(value)
152
+ if isinstance(value, memoryview):
153
+ return value.tobytes()
154
+ if isinstance(value, str):
155
+ return value.encode("utf-8")
156
+ # Raise TypeError for unsupported types to improve type safety
157
+ raise TypeError(
158
+ f"Cannot convert type {type(value).__name__} to bytes. "
159
+ f"Binary() only accepts str, bytes, bytearray, or memoryview objects."
160
+ )