Skip to content

Commit cd94aa4

Browse files
committed
Release 0.7.0
1 parent 2cb5945 commit cd94aa4

File tree

13 files changed

+403
-57
lines changed

13 files changed

+403
-57
lines changed

docs/changelog.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changelog
33
#########
44

5+
Version 0.7.0
6+
=============
7+
8+
* Added support for new FB4 data types (TIME/TIMESTAMP WITH TIMEZONE, DECFLOAT[16|34] and
9+
extended DECIMAL/NUMERIC via INT128 storage).
10+
511
Version 0.6.0
612
=============
713

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
author = 'Pavel Císař'
2424

2525
# The short X.Y version
26-
version = '0.6.0'
26+
version = '0.7.0'
2727

2828
# The full version, including alpha/beta/rc tags
29-
release = '0.6.0'
29+
release = '0.7.0'
3030

3131

3232
# -- General configuration ---------------------------------------------------

docs/ref-intf.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ iDecFloat34
167167

168168
iInt128
169169
-------
170-
..autoclass:: iInt128
170+
.. autoclass:: iInt128
171171

172172
iMaster
173173
-------

docs/ref-main.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ Helper constants:
6666
`DESCRIPTION_INTERNAL_SIZE`, `DESCRIPTION_PRECISION`, `DESCRIPTION_SCALE`
6767
and `DESCRIPTION_NULL_OK`
6868

69+
Helper functions:
70+
`get_timezone`
71+
6972
fbapi
7073
-----
7174

docs/ref-types.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,9 @@ TraceSession
426426
.. autoclass:: TraceSession
427427
:no-members:
428428

429+
Helper functions
430+
================
431+
432+
get_timezone
433+
------------
434+
.. autofunction:: get_timezone

firebird/driver/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
apilevel, threadsafety, paramstyle, DESCRIPTION_NAME, DESCRIPTION_TYPE_CODE, \
5252
DESCRIPTION_DISPLAY_SIZE, DESCRIPTION_INTERNAL_SIZE, DESCRIPTION_PRECISION, \
5353
DESCRIPTION_SCALE, DESCRIPTION_NULL_OK, Date, Time, Timestamp, DateFromTicks, \
54-
TimeFromTicks, TimestampFromTicks, STRING, BINARY, NUMBER, DATETIME, ROWID
54+
TimeFromTicks, TimestampFromTicks, STRING, BINARY, NUMBER, DATETIME, ROWID, \
55+
get_timezone
5556
from .core import connect, create_database, connect_server, transaction, tpb, TPB, \
5657
CHARSET_MAP, DistributedTransactionManager, Connection, Cursor, Server
5758

firebird/driver/core.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2787,8 +2787,18 @@ def _pack_input(self, meta: iMessageMetadata, buffer: bytes,
27872787
memmove(buf_addr + offset, _util.encode_date(value).to_bytes(length, 'little'), length)
27882788
elif datatype == SQLDataType.TIME:
27892789
memmove(buf_addr + offset, _util.encode_time(value).to_bytes(length, 'little'), length)
2790+
elif datatype == SQLDataType.TIME_TZ:
2791+
memmove(buf_addr + offset, _util.encode_time_tz(value), length)
27902792
elif datatype == SQLDataType.TIMESTAMP:
27912793
memmove(buf_addr + offset, _encode_timestamp(value), length)
2794+
elif datatype == SQLDataType.TIMESTAMP_TZ:
2795+
memmove(buf_addr + offset, _util.encode_timestamp_tz(value), length)
2796+
elif datatype == SQLDataType.DEC16:
2797+
memmove(buf_addr + offset, byref(_util.get_decfloat16().from_str(str(value))), length)
2798+
elif datatype == SQLDataType.DEC34:
2799+
memmove(buf_addr + offset, _util.get_decfloat34().from_str(str(value)), length)
2800+
elif datatype == SQLDataType.INT128:
2801+
memmove(buf_addr + offset, _util.get_int128().from_str(str(value), in_meta.get_scale(i)), length)
27922802
elif datatype == SQLDataType.FLOAT:
27932803
memmove(buf_addr + offset, struct.pack('f', value), length)
27942804
elif datatype == SQLDataType.DOUBLE:
@@ -2937,9 +2947,21 @@ def _unpack_output(self) -> Tuple:
29372947
value = _util.decode_date(buffer[offset:offset+length])
29382948
elif datatype == SQLDataType.TIME:
29392949
value = _util.decode_time(buffer[offset:offset+length])
2950+
elif datatype == SQLDataType.TIME_TZ:
2951+
value = _util.decode_time_tz(buffer[offset:offset+length])
29402952
elif datatype == SQLDataType.TIMESTAMP:
29412953
value = datetime.datetime.combine(_util.decode_date(buffer[offset:offset+4]),
29422954
_util.decode_time(buffer[offset+4:offset+length]))
2955+
elif datatype == SQLDataType.TIMESTAMP_TZ:
2956+
value = _util.decode_timestamp_tz(buffer[offset:offset+length])
2957+
elif datatype == SQLDataType.INT128:
2958+
value = decimal.Decimal(_util.get_int128().to_str(a.FB_I128.from_buffer_copy(buffer[offset:offset+length]), desc.scale))
2959+
#elif datatype == SQLDataType.DEC_FIXED:
2960+
#value = 'DEC_FIXED'
2961+
elif datatype == SQLDataType.DEC16:
2962+
value = decimal.Decimal(_util.get_decfloat16().to_str(a.FB_DEC16.from_buffer_copy(buffer[offset:offset+length])))
2963+
elif datatype == SQLDataType.DEC34:
2964+
value = decimal.Decimal(_util.get_decfloat34().to_str(a.FB_DEC34.from_buffer_copy(buffer[offset:offset+length])))
29432965
elif datatype == SQLDataType.FLOAT:
29442966
value = struct.unpack('f', buffer[offset:offset+length])[0]
29452967
elif datatype == SQLDataType.DOUBLE:

firebird/driver/fbapi.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"""
3636
from typing import Union
3737
import sys
38+
import decimal
3839
import ctypes
3940
from ctypes import c_byte, c_ubyte, c_char, c_bool, c_short, c_ushort, c_int, c_uint, \
4041
c_long, c_ulong, c_longlong, c_ulonglong, c_char_p, c_void_p, \
@@ -1904,6 +1905,7 @@ class FirebirdAPI:
19041905
"""
19051906

19061907
def __init__(self, filename: Path = None):
1908+
decimal.getcontext().prec = 34
19071909
if filename is None:
19081910
if sys.platform == 'darwin':
19091911
filename = find_library('Firebird')

firebird/driver/interfaces.py

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
import sys
4040
import threading
4141
import datetime
42-
from ctypes import memmove, memset, create_string_buffer, cast, byref, string_at, \
42+
from ctypes import memmove, memset, create_string_buffer, cast, byref, string_at, sizeof, \
4343
c_char_p, c_void_p, c_byte, c_ulong
4444
from .types import Error, DatabaseError, InterfaceError, BCD, \
4545
StateResult, DirectoryCode, BlobInfoCode, SQLDataType, XpbKind, \
46-
StatementType, StateFlag, CursorFlag, StatementFlag, PreparePrefetchFlag
46+
StatementType, StateFlag, CursorFlag, StatementFlag, PreparePrefetchFlag, get_timezone
4747
from . import fbapi as a
4848
from .hooks import APIHook, add_hook
4949

@@ -1308,64 +1308,108 @@ def set_offsets(self, metadata: iMessageMetadata, callback: iOffsetsCallbackImp)
13081308
class iUtil(iUtil_v2):
13091309
"Class that wraps IUtil v4 interface for use from Python"
13101310
VERSION = 4
1311+
STR_SIZE = 200
1312+
def __init__(self, intf):
1313+
super().__init__(intf)
1314+
self.year = a.Cardinal(0)
1315+
self.month = a.Cardinal(0)
1316+
self.day = a.Cardinal(0)
1317+
self.hours = a.Cardinal(0)
1318+
self.minutes = a.Cardinal(0)
1319+
self.seconds = a.Cardinal(0)
1320+
self.fractions = a.Cardinal(0)
1321+
self.str_buf = create_string_buffer(self.STR_SIZE)
1322+
self.time_tz = create_string_buffer(sizeof(a.ISC_TIME_TZ()))
1323+
self.timestamp_tz = create_string_buffer(sizeof(a.ISC_TIMESTAMP_TZ()))
13111324
def get_decfloat16(self) -> iDecFloat16:
1312-
"TODO"
1325+
"Returns iDecFloat16 interface."
13131326
result = self.vtable.getDecFloat16(self, self.status)
13141327
self._check()
13151328
return iDecFloat16(result)
13161329
def get_decfloat34(self) -> iDecFloat34:
1317-
"TODO"
1330+
"Returns iDecFloat34 interface."
13181331
result = self.vtable.getDecFloat34(self, self.status)
13191332
self._check()
13201333
return iDecFloat34(result)
1321-
def decode_time_tz(self, timetz: a.ISC_TIME_TZ, hours: a.Cardinal, minutes: a.Cardinal,
1322-
seconds: a.Cardinal, fractions: a.Cardinal, zone_bufer: bytes) -> None:
1323-
"TODO"
1334+
def decode_time_tz(self, timetz: Union[a.ISC_TIME_TZ, bytes]) -> datetime.time:
1335+
"Decodes TIME WITH TIMEZONE from internal format to datetime.time with tzinfo."
1336+
if isinstance(timetz, bytes):
1337+
timetz = a.ISC_TIME_TZ.from_buffer_copy(timetz)
1338+
self.hours.value = 0
1339+
self.minutes.value = 0
1340+
self.seconds.value = 0
1341+
self.fractions.value = 0
1342+
memset(self.str_buf, 0, self.STR_SIZE)
13241343
# procedure decodeTimeTz(this: IUtil; status: IStatus; timeTz: ISC_TIME_TZPtr;
13251344
# hours: CardinalPtr; minutes: CardinalPtr;
13261345
# seconds: CardinalPtr; fractions: CardinalPtr;
13271346
# timeZoneBufferLength: Cardinal; timeZoneBuffer: PAnsiChar)
1328-
self.vtable.decodeTimeTz(self, self.status, byref(timetz), byref(hours),
1329-
byref(minutes), byref(seconds), byref(fractions),
1330-
len(zone_bufer), zone_bufer)
1331-
self._check()
1332-
def decode_timestamp_tz(self, timestamptz: a.ISC_TIMESTAMP_TZ, year: a.Cardinal,
1333-
month: a.Cardinal, day: a.Cardinal, hours: a.Cardinal,
1334-
minutes: a.Cardinal, seconds: a.Cardinal, fractions: a.Cardinal,
1335-
zone_bufer: bytes) -> None:
1336-
"TODO"
1347+
self.vtable.decodeTimeTz(self, self.status, byref(timetz), byref(self.hours),
1348+
byref(self.minutes), byref(self.seconds), byref(self.fractions),
1349+
self.STR_SIZE, self.str_buf)
1350+
self._check()
1351+
tz = get_timezone(self.str_buf.value.decode())
1352+
return datetime.time(self.hours.value, self.minutes.value, self.seconds.value,
1353+
self.fractions.value * 100, tz)
1354+
def decode_timestamp_tz(self, timestamptz: Union[a.ISC_TIMESTAMP_TZ, bytes]) -> datetime.datetime:
1355+
"Decodes TIMESTAMP WITH TIMEZONE from internal format to datetime.datetime with tzinfo."
1356+
if isinstance(timestamptz, bytes):
1357+
timestamptz = a.ISC_TIMESTAMP_TZ.from_buffer_copy(timestamptz)
1358+
self.year.value = 0
1359+
self.month.value = 0
1360+
self.day.value = 0
1361+
self.hours.value = 0
1362+
self.minutes.value = 0
1363+
self.seconds.value = 0
1364+
self.fractions.value = 0
1365+
memset(self.str_buf, 0, self.STR_SIZE)
13371366
# procedure decodeTimeStampTz(this: IUtil; status: IStatus; timeStampTz: ISC_TIMESTAMP_TZPtr;
13381367
# year: CardinalPtr; month: CardinalPtr; day: CardinalPtr;
13391368
# hours: CardinalPtr; minutes: CardinalPtr;
13401369
# seconds: CardinalPtr; fractions: CardinalPtr;
13411370
# timeZoneBufferLength: Cardinal; timeZoneBuffer: PAnsiChar)
1342-
self.vtable.decodeTimeStampTz(self, self.status, byref(timestamptz), byref(year),
1343-
byref(month), byref(day), byref(hours),
1344-
byref(minutes), byref(seconds), byref(fractions),
1345-
len(zone_bufer), zone_bufer)
1346-
self._check()
1347-
def encode_time_tz(self, timetz: a.ISC_TIME_TZ, hours: int, minutes: int, seconds: int,
1348-
fractions: int, tz: str) -> None:
1349-
"TODO"
1371+
self.vtable.decodeTimeStampTz(self, self.status, byref(timestamptz), byref(self.year),
1372+
byref(self.month), byref(self.day), byref(self.hours),
1373+
byref(self.minutes), byref(self.seconds), byref(self.fractions),
1374+
self.STR_SIZE, self.str_buf)
1375+
self._check()
1376+
tz = get_timezone(self.str_buf.value.decode())
1377+
return datetime.datetime(self.year.value, self.month.value, self.day.value,
1378+
self.hours.value, self.minutes.value, self.seconds.value,
1379+
self.fractions.value * 100, tz)
1380+
def encode_time_tz(self, time: datetime.time) -> bytes:
1381+
"Encodes datetime.time with tzinfo into internal format for TIME WITH TIMEZONE."
1382+
tzname = getattr(time.tzinfo, '_timezone_', None)
1383+
if not tzname:
1384+
raise InterfaceError("Time timezone not set or does not have a name")
1385+
self.str_buf.value = tzname.encode()
1386+
memset(self.time_tz, 0, 8)
13501387
# procedure encodeTimeTz(this: IUtil; status: IStatus; timeTz: ISC_TIME_TZPtr;
13511388
# hours: Cardinal; minutes: Cardinal; seconds: Cardinal;
13521389
# fractions: Cardinal; timeZone: PAnsiChar)
1353-
self.vtable.encodeTimeTz(self, self.status, byref(timetz), hours, minutes, seconds,
1354-
fractions, tz.encode())
1355-
self._check()
1356-
def encode_timestamp_tz(self, timestamptz: a.ISC_TIMESTAMP_TZ, year: int, month: int,
1357-
day: int, hours: int, minutes: int, seconds: int,
1358-
fractions: int, tz: str) -> None:
1359-
"TODO"
1390+
self.vtable.encodeTimeTz(self, self.status, cast(self.time_tz, a.ISC_TIME_TZ_PTR),
1391+
time.hour, time.minute, time.second, time.microsecond // 100, self.str_buf)
1392+
self._check()
1393+
return self.time_tz.raw
1394+
def encode_timestamp_tz(self, timestamp: datetime.datetime) -> bytes:
1395+
"Encodes datetime.datetime with tzinfo into internal format for TIMESTAMP WITH TIMEZONE."
1396+
tzname = getattr(timestamp.tzinfo, '_timezone_', None)
1397+
if not tzname:
1398+
raise InterfaceError("Datetime timezone not set or does not have a name")
1399+
self.str_buf.value = tzname.encode()
1400+
memset(self.timestamp_tz, 0, 12)
13601401
# procedure encodeTimeStampTz(this: IUtil; status: IStatus; timeStampTz: ISC_TIMESTAMP_TZPtr;
13611402
# year: Cardinal; month: Cardinal; day: Cardinal;
13621403
# hours: Cardinal; minutes: Cardinal; seconds: Cardinal;
13631404
# fractions: Cardinal; timeZone: PAnsiChar)
1364-
self.vtable.encodeTimeStampTz(self, self.status, byref(timestamptz), year, month,
1365-
day, hours, minutes, seconds, fractions, tz.encode())
1405+
self.vtable.encodeTimeStampTz(self, self.status, cast(self.timestamp_tz, a.ISC_TIMESTAMP_TZ_PTR),
1406+
timestamp.year, timestamp.month, timestamp.day,
1407+
timestamp.hour, timestamp.minute, timestamp.second,
1408+
timestamp.microsecond // 100, self.str_buf)
13661409
self._check()
1410+
return self.timestamp_tz.raw
13671411
def get_int128(self) -> iInt128:
1368-
"TODO"
1412+
"Returns iInt128 interface."
13691413
result = self.vtable.getInt128(self, self.status)
13701414
self._check()
13711415
return iInt128(result)
@@ -1489,8 +1533,9 @@ def __init__(self, intf):
14891533
def to_str(self, value: a.FB_I128, scale: int) -> str:
14901534
# procedure toString(this: IInt128; status: IStatus; from: FB_I128Ptr; scale: Integer; bufferLength: Cardinal; buffer: PAnsiChar)
14911535
memset(self.str_buf, 0, self.STR_SIZE)
1492-
self.vtable.toString(self, self.status, byref(value), scale, self.STR_SIZE, buffer)
1536+
self.vtable.toString(self, self.status, byref(value), scale, self.STR_SIZE, self.str_buf)
14931537
self._check()
1538+
return self.str_buf.value.decode()
14941539
def from_str(self, value: str, scale: int, into: a.FB_I128=None) -> a.FB_I128:
14951540
# procedure fromString(this: IInt128; status: IStatus; scale: Integer; from: PAnsiChar; to_: FB_I128Ptr)
14961541
result = a.FB_I128(0) if into is None else into

firebird/driver/types.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import time
4040
import datetime
4141
import decimal
42+
from dateutil import tz
4243
from enum import IntEnum, IntFlag
4344
from dataclasses import dataclass, field
4445
from firebird.base.types import Error
@@ -434,6 +435,9 @@ class SQLDataType(IntEnum):
434435
TIME = 560
435436
DATE = 570
436437
INT64 = 580
438+
TIMESTAMP_TZ_EX = 32748 # Firebird 4
439+
TIME_TZ_EX = 32750 # Firebird 4
440+
INT128 = 32752 # Firebird 4
437441
TIMESTAMP_TZ = 32754 # Firebird 4
438442
TIME_TZ = 32756 # Firebird 4
439443
DEC_FIXED = 32758 # Firebird 4
@@ -1136,17 +1140,17 @@ class TraceSession:
11361140
#: This callable constructs an object holding a time stamp value.
11371141
Timestamp = datetime.datetime
11381142

1139-
def DateFromTicks(ticks: float) -> Date:
1143+
def DateFromTicks(ticks: float) -> Date: # pragma: no cover
11401144
"""Constructs an object holding a date value from the given ticks value
11411145
(number of seconds since the epoch)."""
11421146
return Date(time.localtime(ticks)[:3])
11431147

1144-
def TimeFromTicks(ticks: float) -> Time:
1148+
def TimeFromTicks(ticks: float) -> Time: # pragma: no cover
11451149
"""Constructs an object holding a time value from the given ticks value
11461150
(number of seconds since the epoch)."""
11471151
return Time(time.localtime(ticks)[3:6])
11481152

1149-
def TimestampFromTicks(ticks: float) -> Timestamp:
1153+
def TimestampFromTicks(ticks: float) -> Timestamp: # pragma: no cover
11501154
"""Constructs an object holding a time stamp value from the given ticks value
11511155
(number of seconds since the epoch)."""
11521156
return Timestamp(time.localtime(ticks)[:6])
@@ -1158,7 +1162,7 @@ class DBAPITypeObject:
11581162
"Python DB API 2.0 - type support"
11591163
def __init__(self, *values):
11601164
self.values = values
1161-
def __cmp__(self, other):
1165+
def __cmp__(self, other): # pragma: no cover
11621166
if other in self.values:
11631167
return 0
11641168
if other < self.values:
@@ -1198,3 +1202,20 @@ def is_active(self) -> bool:
11981202
"Returns true if transaction is active"
11991203
...
12001204

1205+
# timezone
1206+
1207+
def get_timezone(timezone: str=None) -> datetime.tzinfo:
1208+
"""Returns `datetime.tzinfo` for specified time zone.
1209+
1210+
This is preferred method to obtain timezone information for construction of timezone-aware
1211+
`datetime.datetime` and `datetime.time` objects. Current implementation uses `dateutil.tz`
1212+
for timezone tzinfo objects, but adds metadata neccessary to store timezone regions into
1213+
database instead zoned time, and to handle offset-based timezones in format required by
1214+
Firebird.
1215+
"""
1216+
if timezone[0] in ['+', '-']:
1217+
timezone = 'UTC' + timezone
1218+
result = tz.gettz(timezone)
1219+
if result is not None and not hasattr(result, '_timezone_'):
1220+
setattr(result, '_timezone_', timezone[3:] if timezone.startswith('UTC') and len(timezone) > 3 else timezone)
1221+
return result

0 commit comments

Comments
 (0)