From 630c17280d1fed1300bd5bfac85e7af1b42c8a02 Mon Sep 17 00:00:00 2001 From: Artyom Smirnov Date: Wed, 28 Nov 2018 19:39:15 +0300 Subject: [PATCH 01/22] Fix Python 2/3 compatibility (#4) --- fdb/fbcore.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index c0be40b..380d4b8 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -30,7 +30,10 @@ import decimal import weakref import threading -from builtins import dict +try: + from builtins import dict +except ImportError: + from __builtin__ import dict from . import ibase from . import schema From 4771847451871021da25fbd5a46bda0f6028a4f6 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Sat, 13 Jul 2019 11:44:22 +0200 Subject: [PATCH 02/22] Documentation for 2.0.1 --- fdb/ibase.py | 2 ++ sphinx/changelog.txt | 25 +++++++++++++++++++++++++ sphinx/conf.py | 4 ++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/fdb/ibase.py b/fdb/ibase.py index ce7c5a9..0724bab 100644 --- a/fdb/ibase.py +++ b/fdb/ibase.py @@ -1397,7 +1397,9 @@ def get_key(key, sub_key): else: fb_library = CDLL(fb_library_name) + #: Firebird client library name self.client_library_name = fb_library_name + #: Firebird client library (loaded by ctypes) self.client_library = fb_library #: isc_attach_database(POINTER(ISC_STATUS), c_short, STRING, POINTER(isc_db_handle), c_short, STRING) diff --git a/sphinx/changelog.txt b/sphinx/changelog.txt index 3c8900f..92e9152 100644 --- a/sphinx/changelog.txt +++ b/sphinx/changelog.txt @@ -3,6 +3,31 @@ Changelog ######### +Version 2.0.1 +============= + +Improvements +------------ + +- Enhancements to :class:`~fdb.BlobReader` - protected data as properties (blob_id, is_text, blob_charset, charset) +- Added support for utf8 database filenames and other string db connection parameters (isc_dpb_utf8_filenames) +- New attribute: :attr:`fdb.ibase.fbclient_API.client_library_name` +- ObjectList: (a) class_type property (b) Allow sort on frozen list + +Other changes +------------- + +- Better readable code with Google-style docstring and better documentation with sphinx-napoleon extension + +Bugs Fixed +---------- + +- (Unregistered) - Bug in ObjectList.any() +- (Unregistered) - fdb.schema: ObjectLists properties for user and system objects of the same type are now stable (not created on getter call) +- (Unregistered) - fdb.log: Fix minor locale problem in Python 3 +- (Unregistered) - fdb.trace: Fix parsing of IPv6 addresses + + Version 2.0 =========== diff --git a/sphinx/conf.py b/sphinx/conf.py index e15f0ca..e6876d5 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -50,9 +50,9 @@ # built documents. # # The short X.Y version. -version = '2.0' +version = '2.0.1' # The full version, including alpha/beta/rc tags. -release = '2.0' +release = '2.0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 224580850d371293d88ed76b62b52a0e5a7ad094 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Sat, 13 Jul 2019 12:20:48 +0200 Subject: [PATCH 03/22] Fix link to documentation --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index af2afaf..c87787b 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', test_suite='nose.collector', project_urls={ - 'Documentation': 'http://fdb2.readthedocs.io/en/latest/', + 'Documentation': 'http://fdb.readthedocs.io/en/latest/', 'Bug Reports': 'http://tracker.firebirdsql.org/browse/PYFB', 'Funding': 'https://www.firebirdsql.org/en/donate/', 'Say Thanks!': 'https://saythanks.io/to/pcisar', From 207e18fb1f1da53159bcd2ca00aaa0d8497a6604 Mon Sep 17 00:00:00 2001 From: Artyom Smirnov Date: Thu, 26 Dec 2019 12:15:55 +0300 Subject: [PATCH 04/22] Properly stuff SPB with timeout parameters for service query (#10) --- fdb/services.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fdb/services.py b/fdb/services.py index 82dba91..3f71403 100644 --- a/fdb/services.py +++ b/fdb/services.py @@ -368,7 +368,11 @@ def _Q(self, code, result_type, timeout=-1): if timeout == -1: spb = ibase.b('') else: - spb = fdb.bs(ibase.isc_info_svc_timeout, timeout) + spb = ctypes.create_string_buffer(8) + spb[0] = ibase.int2byte(ibase.isc_info_svc_timeout) + spb[1:3] = fdb.uint_to_bytes(4, 2) + spb[3:7] = fdb.uint_to_bytes(timeout, 4) + spb[7] = ibase.int2byte(ibase.isc_info_end) while True: result_buffer = ctypes.create_string_buffer(result_size) api.isc_service_query(self._isc_status, self._svc_handle, None, From 133f5c3336d9ed7fef09df7158d74e4a0ec313ca Mon Sep 17 00:00:00 2001 From: Popa Adrian Marius Date: Mon, 20 Jan 2020 15:58:50 +0200 Subject: [PATCH 05/22] =?UTF-8?q?saythanks.io=E2=80=99s=20server=20is=20of?= =?UTF-8?q?fline=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit saythanks.io’s server IP address could not be found. --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cfe6648..6c2576f 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ FDB - The Python driver for Firebird |docs| -Home_ * Documentation_ * `Bug Reports`_ * Source_ * `Say Thanks!`_ +Home_ * Documentation_ * `Bug Reports`_ * Source_ FDB is a `Python`_ library package that implements `Python Database API 2.0`_-compliant support for the open source relational database `Firebird`_ ®. In addition to the minimal feature set of the standard Python DB API, FDB also exposes the entire native @@ -37,7 +37,6 @@ FDB also contains extensive collection of submodules that simplify various Fireb .. _Bug Reports: http://tracker.firebirdsql.org/browse/PYFB .. _Home: http://www.firebirdsql.org/en/devel-python-driver/ .. _Source: https://github.com/FirebirdSQL/fdb -.. _Say Thanks!: https://saythanks.io/to/pcisar .. _Documentation: http://fdb.readthedocs.io/en/v2.0/ .. |donate| image:: https://www.firebirdsql.org/img/donate/donate_to_firebird.gif From 1f0768a96c1dd56577c020c9abdf1931585d747e Mon Sep 17 00:00:00 2001 From: Nikita Bondarenko Date: Sun, 26 Jan 2020 18:24:40 +0300 Subject: [PATCH 06/22] fixed a typo (dns instead dsn) (#13) --- fdb/fbcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 2d7a4af..120e630 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -738,7 +738,7 @@ def connect(dsn='', user=None, password=None, host=None, port=None, database=Non .. important:: - You may specify the database using either `dns` or `database` (with optional `host`), + You may specify the database using either `dsn` or `database` (with optional `host`), but not both. Examples: From fc2eaa29c88ccc9d019214bca37705135e4b2812 Mon Sep 17 00:00:00 2001 From: Dmitry Yemanov Date: Wed, 4 Mar 2020 10:37:43 +0300 Subject: [PATCH 07/22] Fixed (hopefully) usage of the incorrect API function which bypasses the SQL dialect --- fdb/fbcore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 120e630..6c0662c 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -4092,8 +4092,8 @@ def execute_immediate(self, sql): for connection in self._connections: con = connection() sql = b(sql, con._python_charset) - api.isc_execute_immediate(self._isc_status, con._db_handle, self._tr_handle, - ctypes.c_short(len(sql)), sql) + api.isc_dsql_execute_immediate(self._isc_status, con._db_handle, self._tr_handle, + len(sql), sql, self.__sql_dialect, XSQLDA_PTR) if db_api_error(self._isc_status): raise exception_from_status(DatabaseError, self._isc_status, "Error while executing SQL statement:") From 21d97afff3de381cb92b82062a82ce69bd027eec Mon Sep 17 00:00:00 2001 From: Dmitry Yemanov Date: Wed, 4 Mar 2020 20:42:18 +0300 Subject: [PATCH 08/22] Correction --- fdb/fbcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 6c0662c..5e484bf 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -4093,7 +4093,7 @@ def execute_immediate(self, sql): con = connection() sql = b(sql, con._python_charset) api.isc_dsql_execute_immediate(self._isc_status, con._db_handle, self._tr_handle, - len(sql), sql, self.__sql_dialect, XSQLDA_PTR) + len(sql), sql, con.sql_dialect, XSQLDA_PTR) if db_api_error(self._isc_status): raise exception_from_status(DatabaseError, self._isc_status, "Error while executing SQL statement:") From 6abafeaa4a582c083952ed71f130007844cc2a0f Mon Sep 17 00:00:00 2001 From: Dmitry Yemanov Date: Wed, 4 Mar 2020 21:19:50 +0300 Subject: [PATCH 09/22] One more correction --- fdb/fbcore.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 5e484bf..8e2909e 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -4092,8 +4092,13 @@ def execute_immediate(self, sql): for connection in self._connections: con = connection() sql = b(sql, con._python_charset) + xsqlda = xsqlda_factory(1) + + # For yet unknown reason, the isc_dsql_execute_immediate segfaults when + # NULL (None) is passed as XSQLDA, so we provide one here api.isc_dsql_execute_immediate(self._isc_status, con._db_handle, self._tr_handle, - len(sql), sql, con.sql_dialect, XSQLDA_PTR) + len(sql), sql, con.sql_dialect, + ctypes.cast(ctypes.pointer(xsqlda), XSQLDA_PTR)) if db_api_error(self._isc_status): raise exception_from_status(DatabaseError, self._isc_status, "Error while executing SQL statement:") From ad4956fdf60d9d68a08a6a8f501f014a18561f44 Mon Sep 17 00:00:00 2001 From: Russell Stuart Date: Wed, 5 May 2021 20:28:17 +1000 Subject: [PATCH 10/22] Remove apply() - python3 doesn't have it (#97) --- fdb/fbcore.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 8e2909e..5eadaa0 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -319,13 +319,13 @@ def Timestamp(year, month, day, hour, minute, second): return datetime.datetime(year, month, day, hour, minute, second) def DateFromTicks(ticks): - return apply(Date, time.localtime(ticks)[:3]) + return Date(*time.localtime(ticks)[:3]) def TimeFromTicks(ticks): - return apply(Time, time.localtime(ticks)[3:6]) + return Time(*time.localtime(ticks)[3:6]) def TimestampFromTicks(ticks): - return apply(Timestamp, time.localtime(ticks)[:6]) + return Timestamp(*time.localtime(ticks)[:6]) def Binary(b): return b From 1634fe194d74608c988f3df1745464eec120f6fe Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Thu, 30 Sep 2021 19:25:27 +0200 Subject: [PATCH 11/22] Release 2.0.2 --- fdb/fbcore.py | 48 +++++++++++++++++++++++++------------------- fdb/gstat.py | 16 ++++++++------- fdb/log.py | 16 ++++++++------- fdb/schema.py | 2 +- fdb/services.py | 3 ++- setup.py | 0 sphinx/changelog.txt | 48 ++++++++++++++++++++++++++------------------ sphinx/conf.py | 12 +++++------ test/testfdb.py | 40 ++++++++++++++++++------------------ 9 files changed, 103 insertions(+), 82 deletions(-) mode change 100755 => 100644 setup.py diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 5eadaa0..fdde12d 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -156,7 +156,7 @@ PYTHON_MAJOR_VER = sys.version_info[0] #: Current driver version -__version__ = '2.0.1' +__version__ = '2.0.2' apilevel = '2.0' threadsafety = 1 @@ -478,7 +478,10 @@ def tebarray_factory(size): def is_dead_proxy(obj): "Return True if object is a dead :func:`weakref.proxy`." - return isinstance(obj, weakref.ProxyType) and not dir(obj) + try: + return isinstance(obj, weakref.ProxyType) and not dir(obj) + except ReferenceError: + return True def b2u(st, charset): "Decode to unicode if charset is defined. For conversion of result set data." @@ -811,7 +814,7 @@ def build_dpb(user, password, sql_dialect, role, charset, buffers, raise ProgrammingError("SQl Dialect must be either 1, 2 or 3") if ((not dsn and not host and not database) or - (dsn and (host or database)) or + (dsn and (host or database)) or (host and not database)): raise ProgrammingError("Must supply one of:\n" " 1. keyword argument dsn='host:/path/to/database'\n" @@ -935,7 +938,7 @@ def create_database(sql='', sql_dialect=3, dsn='', user=None, password=None, raise ProgrammingError("SQl Dialect must be either 1, 2 or 3") if ((not dsn and not host and not database) or - (dsn and (host or database)) or + (dsn and (host or database)) or (host and not database)): raise ProgrammingError("Must supply one of:\n" " 1. keyword argument dsn='host:/path/to/database'\n" @@ -1321,7 +1324,7 @@ def _determine_field_precision(self, sqlvar): return 0 # Special case for automatic RDB$DB_KEY fields. if ((sqlvar.sqlname_length == 6 and sqlvar.sqlname == 'DB_KEY') or - (sqlvar.sqlname_length == 10 and sqlvar.sqlname == 'RDB$DB_KEY')): + (sqlvar.sqlname_length == 10 and sqlvar.sqlname == 'RDB$DB_KEY')): return 0 precision = self.__precision_cache.get((sqlvar.relname, sqlvar.sqlname)) @@ -2563,7 +2566,7 @@ def __get_rowcount(self): res_walk += short_size count = bytes_to_uint(info[res_walk:res_walk + size]) if ((cur_count_type == isc_info_req_select_count and self.statement_type == isc_info_sql_stmt_select) - or (cur_count_type == isc_info_req_insert_count and self.statement_type == isc_info_sql_stmt_insert) + or (cur_count_type == isc_info_req_insert_count and self.statement_type == isc_info_sql_stmt_insert) or (cur_count_type == isc_info_req_update_count and self.statement_type == isc_info_sql_stmt_update) or (cur_count_type == isc_info_req_delete_count and self.statement_type == isc_info_sql_stmt_delete)): result = count @@ -2707,7 +2710,7 @@ def __xsqlda2tuple(self, xsqlda): #value = sqlvar.sqldata[:sqlvar.sqllen] ### Todo: verify handling of P version differences if ((self.__charset or PYTHON_MAJOR_VER == 3) and - sqlvar.sqlsubtype != 1): # non OCTETS + sqlvar.sqlsubtype != 1): # non OCTETS value = b2u(value, self.__python_charset) # CHAR with multibyte encoding requires special handling if sqlvar.sqlsubtype in (4, 69): # UTF8 and GB18030 @@ -2726,7 +2729,7 @@ def __xsqlda2tuple(self, xsqlda): else: value = str(sqlvar.sqldata[2:2 + size]) if ((self.__charset or PYTHON_MAJOR_VER == 3) and - sqlvar.sqlsubtype != 1): # non OCTETS + sqlvar.sqlsubtype != 1): # non OCTETS value = b2u(value, self.__python_charset) elif vartype == SQL_BOOLEAN: value = bool(bytes_to_int(sqlvar.sqldata.contents.value)) @@ -2810,7 +2813,7 @@ def __xsqlda2tuple(self, xsqlda): offset += length + 2 # Does the blob size exceeds treshold for streamed one? if ((self.__streamed_blob_treshold >= 0) and - (blob_length > self.__streamed_blob_treshold)): + (blob_length > self.__streamed_blob_treshold)): # Stream BLOB value = BlobReader(blobid, self.cursor._connection._db_handle, self.cursor._transaction._tr_handle, @@ -2905,7 +2908,7 @@ def __extract_db_array_to_list(self, esize, dtype, subtype, scale, dim, dimensio val = ctypes.string_at(buf[bufpos:bufpos+esize], esize) ### Todo: verify handling of P version differences if ((self.__charset or PYTHON_MAJOR_VER == 3) - and subtype != 1): # non OCTETS + and subtype != 1): # non OCTETS val = b2u(val, self.__python_charset) # CHAR with multibyte encoding requires special handling if subtype in (4, 69): # UTF8 and GB18030 @@ -2918,7 +2921,7 @@ def __extract_db_array_to_list(self, esize, dtype, subtype, scale, dim, dimensio elif dtype in (blr_varying, blr_varying2): val = ctypes.string_at(buf[bufpos:bufpos+esize]) if ((self.__charset or PYTHON_MAJOR_VER == 3) and - subtype != 1): # non OCTETS + subtype != 1): # non OCTETS val = b2u(val, self.__python_charset) elif dtype in (blr_short, blr_long, blr_int64): val = bytes_to_int(buf[bufpos:bufpos+esize]) @@ -3129,7 +3132,7 @@ def __tuple2xsqlda(self, xsqlda, parameters): sqlvar.sqlind = ctypes.pointer(ISC_SHORT(0)) # Fill in value by type if ((vartype != SQL_BLOB and isinstance(value, (StringType, UnicodeType))) - or vartype in [SQL_TEXT, SQL_VARYING]): + or vartype in [SQL_TEXT, SQL_VARYING]): # Place for Implicit Conversion of Input Parameters # to Strings if not isinstance(value, (UnicodeType, StringType, ibase.mybytes)): @@ -3341,7 +3344,7 @@ def _close(self): if (not connection) or (connection and not connection.closed): api.isc_dsql_free_statement(self._isc_status, stmt_handle, ibase.DSQL_drop) if (db_api_error(self._isc_status) and - (self._isc_status[1] not in [335544528, 335544485])): + (self._isc_status[1] not in [335544528, 335544485])): raise exception_from_status(DatabaseError, self._isc_status, "Error while closing SQL statement:") def _execute(self, parameters=None): @@ -3364,7 +3367,7 @@ def _execute(self, parameters=None): xsqlda_in = None # Execute the statement if ((self.statement_type == isc_info_sql_stmt_exec_procedure) and - (self._out_sqlda.sqld > 0)): + (self._out_sqlda.sqld > 0)): # NOTE: We have to pass xsqlda_out only for statements that return # single row xsqlda_out = ctypes.cast(ctypes.pointer(self._out_sqlda), XSQLDA_PTR) @@ -3548,8 +3551,11 @@ def next(self): def __iter__(self): return self def __valid_ps(self): - return (self._ps is not None) and not (isinstance(self._ps, weakref.ProxyType) - and not dir(self._ps)) + try: + return (self._ps is not None) and not (isinstance(self._ps, weakref.ProxyType) + and not dir(self._ps)) + except ReferenceError: + return False def __get_description(self): if self.__valid_ps(): return self._ps.description @@ -4094,11 +4100,11 @@ def execute_immediate(self, sql): sql = b(sql, con._python_charset) xsqlda = xsqlda_factory(1) - # For yet unknown reason, the isc_dsql_execute_immediate segfaults when - # NULL (None) is passed as XSQLDA, so we provide one here + # For yet unknown reason, the isc_dsql_execute_immediate segfaults when + # NULL (None) is passed as XSQLDA, so we provide one here api.isc_dsql_execute_immediate(self._isc_status, con._db_handle, self._tr_handle, len(sql), sql, con.sql_dialect, - ctypes.cast(ctypes.pointer(xsqlda), XSQLDA_PTR)) + ctypes.cast(ctypes.pointer(xsqlda), XSQLDA_PTR)) if db_api_error(self._isc_status): raise exception_from_status(DatabaseError, self._isc_status, "Error while executing SQL statement:") @@ -4636,7 +4642,7 @@ def __drop_transaction(self): def __require_transaction_state(self, must_be_active, err_msg=''): transaction = self._transaction if ((must_be_active and transaction is None) or - (not must_be_active and (transaction is not None and transaction.active))): + (not must_be_active and (transaction is not None and transaction.active))): raise ProgrammingError(err_msg) def __require_non_empty_group(self, operation_name): if self.count() == 0: @@ -5486,7 +5492,7 @@ def __setitem__(self, key, value): else: other_values = None if ((not isinstance(value, tuple)) - or len(value) != 2 + or len(value) != 2 or value[0] not in (isc_tpb_shared, isc_tpb_protected, isc_tpb_exclusive) or value[1] not in (isc_tpb_lock_read, isc_tpb_lock_write)): raise ValueError("""Table reservation entry must be a 2-tuple of the following form: diff --git a/fdb/gstat.py b/fdb/gstat.py index 44f40f3..e8449ba 100644 --- a/fdb/gstat.py +++ b/fdb/gstat.py @@ -25,7 +25,7 @@ import datetime import weakref from collections import namedtuple -from locale import LC_ALL, getlocale, setlocale, resetlocale +from locale import LC_ALL, LC_CTYPE, getlocale, setlocale, resetlocale import sys GSTAT_25 = 2 @@ -52,6 +52,8 @@ FillDistribution = namedtuple('FillDistribution', 'd20,d40,d50,d80,d100') Encryption = namedtuple('Encryption', 'pages,encrypted,unencrypted') +_LOCALE_ = LC_CTYPE if sys.version_info[0] == 3 else LC_ALL + def empty_str(str_): "Return True if string is empty (whitespace don't count) or None" return true if str_ is None else str_.strip() == '' @@ -573,12 +575,12 @@ def parse_encryption(line): # line_no = 0 step = 0 # Look for sections and skip empty lines + locale = getlocale(_LOCALE_) try: - locale = getlocale(LC_ALL) if sys.platform == 'win32': - setlocale(LC_ALL, 'English_United States') + setlocale(_LOCALE_, 'English_United States') else: - setlocale(LC_ALL, 'en_US') + setlocale(_LOCALE_, 'en_US') # Skip empty lines at start for line in (x.strip() for x in lines): line_no += 1 @@ -654,9 +656,9 @@ def parse_encryption(line): finally: if locale[0] is None: if sys.platform == 'win32': - setlocale(LC_ALL, '') + setlocale(_LOCALE_, '') else: - resetlocale(LC_ALL) + resetlocale(_LOCALE_) else: - setlocale(LC_ALL, locale) + setlocale(_LOCALE_, locale) return db diff --git a/fdb/log.py b/fdb/log.py index 776b767..78ea650 100644 --- a/fdb/log.py +++ b/fdb/log.py @@ -23,11 +23,13 @@ from fdb import ParseError from datetime import datetime from collections import namedtuple -from locale import LC_ALL, getlocale, setlocale, resetlocale +from locale import LC_ALL, LC_CTYPE, getlocale, setlocale, resetlocale import sys LogEntry = namedtuple('LogEntry', 'source_id,timestamp,message') +_LOCALE_ = LC_CTYPE if sys.version_info[0] == 3 else LC_ALL + def parse(lines): """Parse Firebird server log and yield named tuples describing individual log entries/events. @@ -38,11 +40,11 @@ def parse(lines): fdb.ParseError: When any problem is found in input stream. """ line_no = 0 - locale = getlocale() # (LC_ALL) + locale = getlocale() # (_LOCALE_) if sys.platform == 'win32': - setlocale(LC_ALL, 'English_United States') + setlocale(_LOCALE_, 'English_United States') else: - setlocale(LC_ALL, 'en_US') + setlocale(_LOCALE_, 'en_US') try: clean = (line.strip() for line in lines) entry_lines = [] @@ -78,8 +80,8 @@ def parse(lines): finally: if locale[0] is None: if sys.platform == 'win32': - setlocale(LC_ALL, '') + setlocale(_LOCALE_, '') else: - resetlocale(LC_ALL) + resetlocale(_LOCALE_) else: - setlocale(LC_ALL, locale) + setlocale(_LOCALE_, locale) diff --git a/fdb/schema.py b/fdb/schema.py index f6663ac..0949dce 100644 --- a/fdb/schema.py +++ b/fdb/schema.py @@ -1084,7 +1084,7 @@ def get_metadata_ddl(self, sections=SCRIPT_DEFAULT_ORDER): Sections identifiers are represented by `SCRIPT_*` contants defined in schema module. - Sections are created in the order of occerence in list. Uses `SCRIPT_DEFAULT_ORDER` list when sections are not specified. + Sections are created in the order of occurence in list. Uses `SCRIPT_DEFAULT_ORDER` list when sections are not specified. """ def order_by_dependency(items, get_dependencies): ordered = [] diff --git a/fdb/services.py b/fdb/services.py index 3f71403..d6f59dd 100644 --- a/fdb/services.py +++ b/fdb/services.py @@ -828,7 +828,8 @@ def get_statistics(self, database, show_only_db_log_pages=0, tables = (tables,) cmdline = ['-t'] cmdline.extend(tables) - req_buf.add_string(ibase.isc_spb_command_line, ' '.join(cmdline)) + for item in cmdline: + req_buf.add_string(ibase.isc_spb_command_line, item) self._act(req_buf) self.__fetching = True self.__eof = False diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 diff --git a/sphinx/changelog.txt b/sphinx/changelog.txt index 92e9152..5e5c0f8 100644 --- a/sphinx/changelog.txt +++ b/sphinx/changelog.txt @@ -3,6 +3,16 @@ Changelog ######### +Version 2.0.2 +============= + +Bugs Fixed +---------- + +- FirebirdSQL/fdb#96 ReferenceError exception when closing weakly referenced cursor [PYFB81] +- (Unregistered) - Bug in Service get_statistics() when `tables` parameter is specified. +- (Unregistered) - Use proper locale in `gstat` and `log` according to Python version. + Version 2.0.1 ============= @@ -33,12 +43,12 @@ Version 2.0 .. important:: - This is initial release of new *"SweetBitter"* driver generation. - - During this (v2) generation FDB driver will undergo a transition from development centered around Python 2.7 / Firebird 2.x to development centered around Python 3 / Firebird 3. There are some backward incompatible changes between v2 and v1 generation, and you may expect some also between individual releases of second generation. To *soften* this *bitter* pill, the second generation will have new functionality, enhancements and optimalizations gradually added into each public release. - + This is initial release of new *"SweetBitter"* driver generation. + + During this (v2) generation FDB driver will undergo a transition from development centered around Python 2.7 / Firebird 2.x to development centered around Python 3 / Firebird 3. There are some backward incompatible changes between v2 and v1 generation, and you may expect some also between individual releases of second generation. To *soften* this *bitter* pill, the second generation will have new functionality, enhancements and optimalizations gradually added into each public release. + **The second generation is also the last one that will directly support Python 2.7 and will be tested with Firebird 2.** - + The plan is to move forward with v3 generation (Python 3/Firebird 3+) as soon as v2 code base will become mature. Improvements @@ -48,7 +58,7 @@ Improvements - New modules for parsing Firebird trace & audit logs (:mod:`fdb.trace`), gstat output (:mod:`fdb.gstat`) and server log (:mod:`fdb.log`) - Added :class:`fdb.utils.ObjectList` class for improved object collection manipulation. - Modules :mod:`~fdb.monitor` and :mod:`~fdb.schema` now use new :class:`fdb.utils.ObjectList` for collections of information objects. -- Methods :meth:`fdb.Connection.database_info()` and :meth:`fdb.Transaction.transaction_info()` now distinguish between +- Methods :meth:`fdb.Connection.database_info()` and :meth:`fdb.Transaction.transaction_info()` now distinguish between text and binary strings with `result_type` code. - Significant changes to documentation. @@ -100,7 +110,7 @@ Version 1.6 - New: Extended support for database and transaction info (new attributes and functions on :class:`~fdb.Connection` and :class:`~fdb.Transaction`, fixes and improvements to :meth:`~fdb.Connection.db_info` and :meth:`~fdb.Connection.database_info`). - Fix: Missing character sets for automatic translations. -- (PYFB-64_) - cursor.description throws ReferenceError after executemany INSERT +- (PYFB-64_) - cursor.description throws ReferenceError after executemany INSERT Version 1.5.1 ============= @@ -143,7 +153,7 @@ Version 1.4.9 ============= - (PYFB-51_) .get_sql_for('create') returns invalid output parameters -- (PYFB-52_) isc_info* types which are _DATABASE_INFO_CODES_WITH_COUNT_RESULTS raises TypeError: 'float' object cannot be interpreted as an integer +- (PYFB-52_) isc_info* types which are _DATABASE_INFO_CODES_WITH_COUNT_RESULTS raises TypeError: 'float' object cannot be interpreted as an integer Version 1.4.8 ============= @@ -202,13 +212,13 @@ Improvements ------------ - In relation to PYFB-43_ I had to make a **backward incompatible change** to event processing API. Starting from this version - `EventConduit` does not automatically starts collection of events upon creation, but it's now necessary to call + `EventConduit` does not automatically starts collection of events upon creation, but it's now necessary to call :meth:`~fdb.EventConduit.begin` method. To mitigate the inconvenience, EventConduit now supports context manager protocol that ensures calls to begin() and close() via `with` statement. - In relation to PYFB-39_ I have decided to drop support for implicitly cached and reused prepared statements. I never - liked this feature as I think it's a sneaky method how to put some performance to badly written applications that + liked this feature as I think it's a sneaky method how to put some performance to badly written applications that in worst case may lead to significant resource consumption on server side when developers are not only lazy but also stupid. - It was implemented for the sake of compatibility with KInterbasDB. + It was implemented for the sake of compatibility with KInterbasDB. **This change has no impact on API, but may affect performance of your applications.** @@ -228,7 +238,7 @@ Improvements - PYFB-40_ - fbclient.dll is not found if not in path. Aside from registry lookup, client library isn't loaded until first call to :func:`fdb.connect`, :func:`fdb.create_database` - or :func:`fdb.load_api` (which now supports optional specification of Firebird Client Library to load). + or :func:`fdb.load_api` (which now supports optional specification of Firebird Client Library to load). - Adjustments for Firebird 3.0 (Alpha1) - Properties :attr:`~fdb.services.Connection.version` and :attr:`~fdb.services.Connection.engine_version` added to :class:`fdb.services.Connection` @@ -288,14 +298,14 @@ New Features - :mod:`fdb.schema` submodule for access to / work with database metadata. - :mod:`fdb.utils` submodule with various helper classes and functions. - New :attr:`fdb.Connection.schema` property for access to database schema. -- New :class:`~fdb.ConnectionWithSchema` connection class that provides more direct access +- New :class:`~fdb.ConnectionWithSchema` connection class that provides more direct access to database schema than `Connection`. -- New :attr:`fdb.Connection.firebird_version`, :attr:`fdb.Connection.version` and +- New :attr:`fdb.Connection.firebird_version`, :attr:`fdb.Connection.version` and :attr:`fdb.Connection.engine_version` properties. - New `Connection.ods` read only property that returns ODS version number of connected database. There are also new module-level constants `ODS_FB_20`, `ODS_FB_21` and `ODS_FB_25`. -- New :attr:`fdb.Connection.query_transaction` property. This is ReadOnly ReadCommitted transaction - that could be active indefinitely without blocking garbage collection. It’s used internally to query +- New :attr:`fdb.Connection.query_transaction` property. This is ReadOnly ReadCommitted transaction + that could be active indefinitely without blocking garbage collection. It’s used internally to query metadata, but it’s generally useful. Improvements @@ -305,13 +315,13 @@ Improvements - `Connection.Error`, `Connection.ProgrammingError`, etc. - All exception classes defined by the DB API standard are exposed on the Connection objects + All exception classes defined by the DB API standard are exposed on the Connection objects as attributes (in addition to being available at module scope). - `Cursor.connection` This read-only attribute return a reference to the Connection object on which the cursor was created. -- `Cursor.transaction` read-only attribute returns a reference to the Transaction object on which the cursor +- `Cursor.transaction` read-only attribute returns a reference to the Transaction object on which the cursor was created. - Optimized wekref management, especially for `PreparedStatement`. - `create_database` now supports two methods for database screation. You can specify CREATE DATABASE statement @@ -459,7 +469,7 @@ Version 0.7 Initial release. -Almost feature-complete (ready for 95% of users), but it could be still buggy (it’s beta!), and the +Almost feature-complete (ready for 95% of users), but it could be still buggy (it’s beta!), and the code wasn’t optimized for size and speed. In all other ways it’s ready for wide testing. What’s missing diff --git a/sphinx/conf.py b/sphinx/conf.py index e6876d5..01462ee 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -43,16 +43,16 @@ # General information about the project. project = u'FDB' -copyright = u'2009-2018 Pavel Cisar' +copyright = u'2009-2021 Pavel Cisar' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.0.1' +version = '2.0.2' # The full version, including alpha/beta/rc tags. -release = '2.0.1' +release = '2.0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -97,7 +97,7 @@ # Intersphinx options intersphinx_mapping = {'python': ('http://docs.python.org/2/', None)} - + # Options for HTML output # ----------------------- @@ -265,8 +265,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ - ('index', 'FDB.tex', ur'FDB Documentation', - ur'Pavel Cisar', 'manual'), + ('index', 'FDB.tex', u'FDB Documentation', + u'Pavel Císař', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/test/testfdb.py b/test/testfdb.py index 778d3ec..c8e26d3 100644 --- a/test/testfdb.py +++ b/test/testfdb.py @@ -37,9 +37,9 @@ from contextlib import closing from re import finditer from pprint import pprint -from fdb.gstat import FillDistribution, Encryption, StatDatabase +from fdb.gstat import FillDistribution, Encryption, StatDatabase, _LOCALE_ from fdb.log import LogEntry -from locale import LC_ALL, getlocale, setlocale, getdefaultlocale +from locale import getlocale, setlocale, getdefaultlocale if ibase.PYTHON_MAJOR_VER == 3: from io import StringIO, BytesIO @@ -8725,23 +8725,23 @@ def _parse_file(self, filename): with open(filename) as f: return gstat.parse(f) def test_locale(self): - locale = getlocale(LC_ALL) + locale = getlocale(_LOCALE_) if locale[0] is None: - setlocale(LC_ALL,'') - locale = getlocale(LC_ALL) + setlocale(_LOCALE_,'') + locale = getlocale(_LOCALE_) try: db = self._parse_file(os.path.join(self.dbpath, 'gstat25-h.out')) - self.assertEquals(locale, getlocale(LC_ALL), "Locale must not change") + self.assertEquals(locale, getlocale(_LOCALE_), "Locale must not change") if sys.platform == 'win32': - setlocale(LC_ALL, 'Czech_Czech Republic') + setlocale(_LOCALE_, 'Czech_Czech Republic') else: - setlocale(LC_ALL, 'cs_CZ') - nlocale = getlocale(LC_ALL) + setlocale(_LOCALE_, 'cs_CZ') + nlocale = getlocale(_LOCALE_) db = self._parse_file(os.path.join(self.dbpath, 'gstat25-h.out')) - self.assertEquals(nlocale, getlocale(LC_ALL), "Locale must not change") + self.assertEquals(nlocale, getlocale(_LOCALE_), "Locale must not change") finally: pass - #setlocale(LC_ALL, locale) + #setlocale(_LOCALE_, locale) def test_parse25_h(self): db = self._parse_file(os.path.join(self.dbpath, 'gstat25-h.out')) data = {'attributes': (0,), 'backup_diff_file': None, 'backup_guid': None, 'bumped_transaction': 1, @@ -10811,23 +10811,23 @@ def test_locale(self): """ output = """LogEntry(source_id='SRVDB1', timestamp=datetime.datetime(2017, 4, 4, 21, 25, 40), message='INET/inet_error: read errno = 10054') """ - locale = getlocale(LC_ALL) + locale = getlocale(_LOCALE_) if locale[0] is None: - setlocale(LC_ALL,'') - locale = getlocale(LC_ALL) + setlocale(_LOCALE_,'') + locale = getlocale(_LOCALE_) try: self._check_events(data, output) - self.assertEquals(locale, getlocale(LC_ALL), "Locale must not change") + self.assertEquals(locale, getlocale(_LOCALE_), "Locale must not change") self.clear_output() if sys.platform == 'win32': - setlocale(LC_ALL, 'Czech_Czech Republic') + setlocale(_LOCALE_, 'Czech_Czech Republic') else: - setlocale(LC_ALL, 'cs_CZ') - nlocale = getlocale(LC_ALL) + setlocale(_LOCALE_, 'cs_CZ') + nlocale = getlocale(_LOCALE_) self._check_events(data, output) - self.assertEquals(nlocale, getlocale(LC_ALL), "Locale must not change") + self.assertEquals(nlocale, getlocale(_LOCALE_), "Locale must not change") finally: - setlocale(LC_ALL, locale) + setlocale(_LOCALE_, locale) def TestWindowsService(self): data = """ From afa12864aa660a6600549f8084ec2ff0f105d016 Mon Sep 17 00:00:00 2001 From: Artyom Smirnov Date: Tue, 7 Feb 2023 12:00:22 +0300 Subject: [PATCH 12/22] Add fb_shutdown_callback and fb_shutdown to internal API (#17) To allow control shutdown process of driver. For example: from fdb.ibase import FB_SHUTDOWN_CALLBACK, fb_shut_confirmation, fb_shutrsn_signal, fb_shutrsn_app_stopped @FB_SHUTDOWN_CALLBACK def shutdown_callback(reason, mask, arg): if reason == fb_shutrsn_signal: return 1 return 0 status = fdb.ISC_STATUS(0) fdb.fbcore.api.fb_shutdown_callback(status, shutdown_callback, fb_shut_confirmation, None) ... fdb.fbcore.api.fb_shutdown(0, fb_shutrsn_app_stopped) --- fdb/ibase.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fdb/ibase.py b/fdb/ibase.py index 0724bab..0959baf 100644 --- a/fdb/ibase.py +++ b/fdb/ibase.py @@ -2188,6 +2188,15 @@ def get_key(key, sub_key): #: C_isc_event_block(ISC_LONG, POINTER(POINTER(ISC_UCHAR)), POINTER(POINTER(ISC_UCHAR)), ISC_USHORT) self.C_isc_event_block = self.P_isc_event_block(('isc_event_block', fb_library)) self.P_isc_event_block_args = self.C_isc_event_block.argtypes + #: fb_shutdown_callback(POINTER(ISC_STATUS), FB_SHUTDOWN_CALLBACK, c_int, c_void_p) + self.fb_shutdown_callback = fb_library.fb_shutdown_callback + self.fb_shutdown_callback.restype = ISC_STATUS + self.fb_shutdown_callback.argtypes = [POINTER(ISC_STATUS), FB_SHUTDOWN_CALLBACK, + c_int, c_void_p] + #: fb_shutdown(c_int, c_uint, c_int) + self.fb_shutdown = fb_library.fb_shutdown + self.fb_shutdown.restype = c_int + self.fb_shutdown.argtypes = [c_uint, c_int] def isc_event_block(self, event_buffer, result_buffer, *args): "Injects variable number of parameters into C_isc_event_block call" From 8b6ef94e852663bf047753fb22486dcc59a4190d Mon Sep 17 00:00:00 2001 From: Rafael Calixto Date: Fri, 30 Jun 2023 11:07:17 -0300 Subject: [PATCH 13/22] Update fbcore.py (#3) My System works on UTF-8, but my Firebird send me messages in LATIN-1. So I added a handler for errors in the encode function for the application don't break with those messages. --- fdb/fbcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index fdde12d..ca482f9 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -604,7 +604,7 @@ def exception_from_status(error, status, preamble=None): result = api.fb_interpret(msg, 512, pvector) if result != 0: if PYTHON_MAJOR_VER == 3: - msglist.append('- ' + (msg.value).decode(sys_encoding)) + msglist.append('- ' + (msg.value).decode(sys_encoding, errors = 'replace')) else: msglist.append('- ' + msg.value) else: From 27e0f25c45a8310f8b657c5e08cdd6e656f92970 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Fri, 30 Jun 2023 16:23:27 +0200 Subject: [PATCH 14/22] Fix for #114 --- fdb/fbcore.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index ca482f9..938b795 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -486,7 +486,7 @@ def is_dead_proxy(obj): def b2u(st, charset): "Decode to unicode if charset is defined. For conversion of result set data." if charset: - return st.decode(charset) + return st.decode(charset, errors='replace') else: return st @@ -604,7 +604,7 @@ def exception_from_status(error, status, preamble=None): result = api.fb_interpret(msg, 512, pvector) if result != 0: if PYTHON_MAJOR_VER == 3: - msglist.append('- ' + (msg.value).decode(sys_encoding, errors = 'replace')) + msglist.append('- ' + (msg.value).decode(sys_encoding, errors='replace')) else: msglist.append('- ' + msg.value) else: @@ -2151,16 +2151,20 @@ def begin(self): Must be called directly or through context manager interface.""" def event_process(queue): - while True: - operation, data = queue.get() - if operation == ibase.OP_RECORD_AND_REREGISTER: - events = data.count_and_reregister() - if events: - for key, value in events.items(): - self.__events[key] += value - self.__events_ready.set() - elif operation == ibase.OP_DIE: - return + try: + while True: + operation, data = queue.get() + if operation == ibase.OP_RECORD_AND_REREGISTER: + events = data.count_and_reregister() + if events: + for key, value in events.items(): + self.__events[key] += value + self.__events_ready.set() + elif operation == ibase.OP_DIE: + return + except: + self.__events_ready.set() + self.__closed = True self.__initialized = True self.__process_thread = threading.Thread(target=event_process, args=(self.__queue,)) From ddaa1aead8ef5325f795cf0b5ac9dd3a81304637 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Tue, 26 Sep 2023 19:00:13 +0200 Subject: [PATCH 15/22] Move to new rtfd configuration format --- .readthedocs.yml | 17 +++++++++++++++++ requirements.txt | 2 -- sphinx/conf.py | 7 ++++--- sphinx/requirements.txt | 4 ++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .readthedocs.yml delete mode 100644 requirements.txt create mode 100644 sphinx/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..9c02c4b --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,17 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: sphinx/conf.py + + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.8 + install: + - requirements: sphinx/requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 81fc4e6..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -future>=0.16.0 -sphinx-bootstrap-theme==0.6.5 diff --git a/sphinx/conf.py b/sphinx/conf.py index 01462ee..5b64731 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -16,6 +16,7 @@ import sys, os import sphinx_bootstrap_theme +from fdb import __version__ # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it @@ -43,16 +44,16 @@ # General information about the project. project = u'FDB' -copyright = u'2009-2021 Pavel Cisar' +copyright = u'2009-present Pavel Cisar' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.0.2' +version = __version__ # The full version, including alpha/beta/rc tags. -release = '2.0.2' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/sphinx/requirements.txt b/sphinx/requirements.txt new file mode 100644 index 0000000..ab73e38 --- /dev/null +++ b/sphinx/requirements.txt @@ -0,0 +1,4 @@ +sphinx-bootstrap-theme>=0.8.1 +sphinx-autodoc-typehints>=1.24.0 +sphinx>=5.3 +. From 12ee51fae46a769f228d0de1ca4d3da871161460 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Thu, 21 Dec 2023 17:42:59 +0100 Subject: [PATCH 16/22] New config --- .readthedocs.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 9c02c4b..7ebaed3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,9 +9,12 @@ version: 2 sphinx: configuration: sphinx/conf.py - +build: + os: "ubuntu-22.04" + tools: + python: "3.11" + # Optionally set the version of Python and requirements required to build your docs python: - version: 3.8 install: - requirements: sphinx/requirements.txt From 1b51153aa239b06d693f00d07dcbb55ad6c168eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=A4chter?= Date: Sat, 1 Feb 2025 10:42:58 +0100 Subject: [PATCH 17/22] fixed #117 to be Python 3.13 compatible (#119) Co-authored-by: TGSOFTGMBH\sven.gaechter --- fdb/gstat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdb/gstat.py b/fdb/gstat.py index e8449ba..671f535 100644 --- a/fdb/gstat.py +++ b/fdb/gstat.py @@ -25,7 +25,7 @@ import datetime import weakref from collections import namedtuple -from locale import LC_ALL, LC_CTYPE, getlocale, setlocale, resetlocale +from locale import LC_ALL, LC_CTYPE, getlocale, setlocale import sys GSTAT_25 = 2 @@ -658,7 +658,7 @@ def parse_encryption(line): if sys.platform == 'win32': setlocale(_LOCALE_, '') else: - resetlocale(_LOCALE_) + setlocale(LC_ALL, '') else: setlocale(_LOCALE_, locale) return db From 4ad8c4101f239fdad8ac514a5c393b0b795bbe7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20C=C3=ADsa=C5=99?= Date: Tue, 22 Apr 2025 19:18:59 +0200 Subject: [PATCH 18/22] Update README.rst --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 6c2576f..9769723 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,12 @@ FDB - The Python driver for Firebird ==================================== +[![PyPI - Version](https://img.shields.io/pypi/v/fdb.svg)](https://pypi.org/project/fdb) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fdb.svg)](https://pypi.org/project/fdb) +[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/fdb)](https://pypi.org/project/fdb) +[![Libraries.io SourceRank](https://img.shields.io/librariesio/sourcerank/pypi/fdb)](https://libraries.io/pypi/fdb) + |docs| Home_ * Documentation_ * `Bug Reports`_ * Source_ From df3b5ee2f896967a8e4b5bfaac17519a726d2bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20C=C3=ADsa=C5=99?= Date: Tue, 22 Apr 2025 19:23:51 +0200 Subject: [PATCH 19/22] Update README.rst --- README.rst | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 9769723..3dfb52e 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,7 @@ FDB - The Python driver for Firebird ==================================== -[![PyPI - Version](https://img.shields.io/pypi/v/fdb.svg)](https://pypi.org/project/fdb) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fdb.svg)](https://pypi.org/project/fdb) -[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/fdb)](https://pypi.org/project/fdb) -[![Libraries.io SourceRank](https://img.shields.io/librariesio/sourcerank/pypi/fdb)](https://libraries.io/pypi/fdb) - -|docs| +|docs| |version| |python| |hatch| |downloads| |rank| Home_ * Documentation_ * `Bug Reports`_ * Source_ @@ -55,4 +49,27 @@ FDB also contains extensive collection of submodules that simplify various Fireb :scale: 100% :target: http://fdb.readthedocs.io/en/v2.0/ +.. |version| image:: https://img.shields.io/pypi/v/fdb.svg + :alt: Version + :scale: 100% + :target: https://pypi.org/project/fdb +.. |python| image:: https://img.shields.io/pypi/pyversions/fdb.svg + :alt: Python version + :scale: 100% + :target: https://pypi.org/project/fdb + +.. |hatch| image:: https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg + :alt: Hatch + :scale: 100% + :target: https://pypi.org/project/fdb + +.. |downloads| image:: https://img.shields.io/pypi/dm/fdb + :alt: Downloads + :scale: 100% + :target: https://pypi.org/project/fdb + +.. |rank| image:: https://img.shields.io/librariesio/sourcerank/pypi/fdb + :alt: rank + :scale: 100% + :target: https://libraries.io/pypi/fdb From d591101710591c721599c6ff10a4cf8b564431d9 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Sat, 3 May 2025 14:36:37 +0200 Subject: [PATCH 20/22] Version 2.0.3 --- LICENSE.TXT => LICENSE | 36 ++++++------- README.md | 41 +++++++++++++++ README.rst | 75 --------------------------- fdb/blr.py | 50 +++++++++--------- fdb/fbcore.py | 2 +- pyproject.toml | 57 ++++++++++++++++++++ setup.cfg | 20 ------- setup.py | 65 ----------------------- sphinx/changelog.txt | 11 ++++ {test => tests}/fbtest.fdb | Bin {test => tests}/fbtest20.fdb | Bin {test => tests}/fbtest21.fdb | Bin {test => tests}/fbtest25.fdb | Bin 1327104 -> 1331200 bytes {test => tests}/fbtest30.fdb | Bin 3006464 -> 3006464 bytes {test => tests}/gstat25-a.out | 0 {test => tests}/gstat25-d.out | 0 {test => tests}/gstat25-f.out | 0 {test => tests}/gstat25-h.out | 0 {test => tests}/gstat25-i.out | 0 {test => tests}/gstat25-r.out | 0 {test => tests}/gstat25-s.out | 0 {test => tests}/gstat30-a.out | 0 {test => tests}/gstat30-d.out | 0 {test => tests}/gstat30-e.out | 0 {test => tests}/gstat30-f.out | 0 {test => tests}/gstat30-h.out | 0 {test => tests}/gstat30-i.out | 0 {test => tests}/gstat30-r.out | 0 {test => tests}/gstat30-s.out | 0 test/testfdb.py => tests/test_fdb.py | 11 ++-- 30 files changed, 159 insertions(+), 209 deletions(-) rename LICENSE.TXT => LICENSE (82%) create mode 100644 README.md delete mode 100644 README.rst create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py rename {test => tests}/fbtest.fdb (100%) rename {test => tests}/fbtest20.fdb (100%) rename {test => tests}/fbtest21.fdb (100%) rename {test => tests}/fbtest25.fdb (97%) rename {test => tests}/fbtest30.fdb (96%) rename {test => tests}/gstat25-a.out (100%) rename {test => tests}/gstat25-d.out (100%) rename {test => tests}/gstat25-f.out (100%) rename {test => tests}/gstat25-h.out (100%) rename {test => tests}/gstat25-i.out (100%) rename {test => tests}/gstat25-r.out (100%) rename {test => tests}/gstat25-s.out (100%) rename {test => tests}/gstat30-a.out (100%) rename {test => tests}/gstat30-d.out (100%) rename {test => tests}/gstat30-e.out (100%) rename {test => tests}/gstat30-f.out (100%) rename {test => tests}/gstat30-h.out (100%) rename {test => tests}/gstat30-i.out (100%) rename {test => tests}/gstat30-r.out (100%) rename {test => tests}/gstat30-s.out (100%) rename test/testfdb.py => tests/test_fdb.py (99%) diff --git a/LICENSE.TXT b/LICENSE similarity index 82% rename from LICENSE.TXT rename to LICENSE index b6ed28f..60ac66f 100644 --- a/LICENSE.TXT +++ b/LICENSE @@ -2,20 +2,20 @@ LICENSE ======= -The following contributors hold Copyright (c) over their respective portions +The following contributors hold Copyright (c) over their respective portions of code and documentation: -The main portion of initial code (~95%); Current maintainer: +The main portion of initial code (~95%); Current maintainer: - Pavel Cisar + Pavel Cisar Contributors: [Initial trace API & nbackup support; Python 3 support] Philippe Makowski -Some code in inital version is Python code from KInterbasDB 3.3.0. -As it's very hard to find out who exactly was the original author of used code, +Some code in inital version is Python code from KInterbasDB 3.3.0. +As it's very hard to find out who exactly was the original author of used code, here is the full list of KInterbasDB contributors: [Author of original version; maintained through version 2.0:] @@ -28,34 +28,34 @@ here is the full list of KInterbasDB contributors: [Finishing touch to v3.3; New Documentation; Current maintainer:] - 2008-2011 [paci] Pavel Cisar + 2008-2011 [paci] Pavel Cisar [Significant Contributors:] 2001-2002 [maz] Marek Isalski - Marek made important first steps in removing the limitations of version 2.0 + Marek made important first steps in removing the limitations of version 2.0 in preparation for version 3.0. 2001 [eac] Evgeny A. Cherkashin - Evgeny wrote the first version of the distutils build script, + Evgeny wrote the first version of the distutils build script, which was included in a 2.x point release. 2001-2002 [janez] Janez Jere - Janez contributed several bugfixes, including fixes for the date and time + Janez contributed several bugfixes, including fixes for the date and time parameter conversion code in preparation for version 3.0. -Permission to use, copy, modify, and distribute this software and its documentation -for any purpose and without fee or royalty is hereby granted, provided that the above -copyright notice appears in all copies and that both the copyright notice and -this permission notice appear in supporting documentation or portions thereof, +Permission to use, copy, modify, and distribute this software and its documentation +for any purpose and without fee or royalty is hereby granted, provided that the above +copyright notice appears in all copies and that both the copyright notice and +this permission notice appear in supporting documentation or portions thereof, including modifications, that you make. -The authors disclaim all warranties with regard to this software, including all -implied warranties of merchantability and fitness. In no event shall any author -be liable for any special, indirect or consequential damages or any damages whatsoever -resulting from loss of use, data or profits, whether in an action of contract, -negligence or other tortious action, arising out of or in connection with the use +The authors disclaim all warranties with regard to this software, including all +implied warranties of merchantability and fitness. In no event shall any author +be liable for any special, indirect or consequential damages or any damages whatsoever +resulting from loss of use, data or profits, whether in an action of contract, +negligence or other tortious action, arising out of or in connection with the use or performance of this software. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea272d0 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# FDB - legacy Firebird driver for Python + +[![PyPI - Version](https://img.shields.io/pypi/v/fdb.svg)](https://pypi.org/project/fdb) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fdb.svg)](https://pypi.org/project/fdb) +[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/fdb)](https://pypi.org/project/fdb) +[![Libraries.io SourceRank](https://img.shields.io/librariesio/sourcerank/pypi/fdb)](https://libraries.io/pypi/fdb) + +## Important + +This package provides LEGACY Python Database API 2.0-compliant driver for the open +source relational database **Firebird version 2.5.**, with **limited support for Firebird 3.0**. +It does NOT support Firebird 4 and newer. + +If you are using Firebird 3 or newer, please use the OFFICIAL Python driver for Firebird +called [firebird-driver](https://pypi.org/project/firebird-driver/). + +The FDB driver is not actively developed and its maintenance is very limited. + +----- + +**Table of Contents** + +- [Installation](#installation) +- [License](#license) +- [Documentation](#documentation) + +## Installation + +```console +pip install fdb +``` + +## License + +`fdb` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. + +## Documentation + +The documentation for this package is available at [https://fdb.readthedocs.io](https://fdb.readthedocs.io) + diff --git a/README.rst b/README.rst deleted file mode 100644 index 3dfb52e..0000000 --- a/README.rst +++ /dev/null @@ -1,75 +0,0 @@ -==================================== -FDB - The Python driver for Firebird -==================================== - -|docs| |version| |python| |hatch| |downloads| |rank| - -Home_ * Documentation_ * `Bug Reports`_ * Source_ - -FDB is a `Python`_ library package that implements `Python Database API 2.0`_-compliant support for the open source relational -database `Firebird`_ ®. In addition to the minimal feature set of the standard Python DB API, FDB also exposes the entire native -(old-style) client API of the database engine. Notably: - -* Automatic data conversion from strings on input. -* Automatic input/output conversions of textual data between UNICODE and database character sets. -* Support for prepared SQL statements. -* Multiple independent transactions per single connection. -* All transaction parameters that Firebird supports, including table access specifications. -* Distributed transactions. -* Firebird BLOB support, including support for stream BLOBs. -* Support for Firebird Events. -* Support for Firebird ARRAY data type. -* Support for all Firebird Services - -FDB also contains extensive collection of submodules that simplify various Firebird-related tasks. Notably: - -* Database schema -* Firebird monitoring tables -* Parsing Firebird trace & audit logs -* Parsing Firebird server log -* Parsing Firebird gstat utility output - -|donate| - -.. _Python: http://python.org -.. _Python Database API 2.0: http://www.python.org/dev/peps/pep-0249/ -.. _Firebird: http://www.firebirdsql.org -.. _Bug Reports: http://tracker.firebirdsql.org/browse/PYFB -.. _Home: http://www.firebirdsql.org/en/devel-python-driver/ -.. _Source: https://github.com/FirebirdSQL/fdb -.. _Documentation: http://fdb.readthedocs.io/en/v2.0/ - -.. |donate| image:: https://www.firebirdsql.org/img/donate/donate_to_firebird.gif - :alt: Contribute to the development - :scale: 100% - :target: https://www.firebirdsql.org/en/donate/ - -.. |docs| image:: https://readthedocs.org/projects/fdb/badge/?version=v2.0 - :alt: Documentation Status - :scale: 100% - :target: http://fdb.readthedocs.io/en/v2.0/ - -.. |version| image:: https://img.shields.io/pypi/v/fdb.svg - :alt: Version - :scale: 100% - :target: https://pypi.org/project/fdb - -.. |python| image:: https://img.shields.io/pypi/pyversions/fdb.svg - :alt: Python version - :scale: 100% - :target: https://pypi.org/project/fdb - -.. |hatch| image:: https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg - :alt: Hatch - :scale: 100% - :target: https://pypi.org/project/fdb - -.. |downloads| image:: https://img.shields.io/pypi/dm/fdb - :alt: Downloads - :scale: 100% - :target: https://pypi.org/project/fdb - -.. |rank| image:: https://img.shields.io/librariesio/sourcerank/pypi/fdb - :alt: rank - :scale: 100% - :target: https://libraries.io/pypi/fdb diff --git a/fdb/blr.py b/fdb/blr.py index dc537f0..f8f2b39 100644 --- a/fdb/blr.py +++ b/fdb/blr.py @@ -23,31 +23,31 @@ # BLR data types are defined in fdb.ibase -#blr_text = 14 -#blr_text2 = 15 -#blr_short = 7 -#blr_long = 8 -#blr_quad = 9 -#blr_float = 10 -#blr_double = 27 -#blr_d_float = 11 -#blr_timestamp = 35 -#blr_varying = 37 -#blr_varying2 = 38 -#blr_blob = 261 -#blr_cstring = 40 -#blr_cstring2 = 41 -#blr_blob_id = 45 -#blr_sql_date = 12 -#blr_sql_time = 13 -#blr_int64 = 16 -#blr_blob2 = 17 -#blr_domain_name = 18 -#blr_domain_name2 = 19 -#blr_not_nullable = 20 -#blr_column_name = 21 -#blr_column_name2 = 22 -#blr_bool = 23 # Firebird 3.0 +blr_text = 14 +blr_text2 = 15 +blr_short = 7 +blr_long = 8 +blr_quad = 9 +blr_float = 10 +blr_double = 27 +blr_d_float = 11 +blr_timestamp = 35 +blr_varying = 37 +blr_varying2 = 38 +blr_blob = 261 +blr_cstring = 40 +blr_cstring2 = 41 +blr_blob_id = 45 +blr_sql_date = 12 +blr_sql_time = 13 +blr_int64 = 16 +blr_blob2 = 17 +blr_domain_name = 18 +blr_domain_name2 = 19 +blr_not_nullable = 20 +blr_column_name = 21 +blr_column_name2 = 22 +blr_bool = 23 # Firebird 3.0 # first sub parameter for blr_domain_name[2] blr_domain_type_of = 0 diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 938b795..575fd16 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -156,7 +156,7 @@ PYTHON_MAJOR_VER = sys.version_info[0] #: Current driver version -__version__ = '2.0.2' +__version__ = '2.0.3' apilevel = '2.0' threadsafety = 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0110631 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "fdb" +description = "Legacy Python driver for Firebird 2.5" +dynamic = ["version"] +readme = "README.md" +requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +license = { file = "LICENSE" } +authors = [ + { name = "Pavel Cisar", email = "pcisar@users.sourceforge.net"}, +] +keywords = ["Firebird", "RDBMS", "driver"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Topic :: Software Development", + "Topic :: Database", +] +dependencies = [ + "firebird-base~=2.0", + "python-dateutil~=2.8", +] + +[project.urls] +Home = "https://github.com/FirebirdSQL/fdb" +Documentation = "https://fdb.rtfd.io" +Issues = "https://github.com/FirebirdSQL/fdb/issues" +Source = "https://github.com/FirebirdSQL/fdb" + +[tool.hatch.version] +path = "fdb/fbcore.py" + +[tool.hatch.build.targets.sdist] +include = ["fdb"] + +[tool.hatch.build.targets.wheel] +packages = ["fdb"] + +[tool.hatch.envs.default] +dependencies = [ +] + +[[tool.hatch.envs.hatch-test.matrix]] +python = ["2.7", "3.11"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 08a1c03..0000000 --- a/setup.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - -[build_sphinx] -source-dir=sphinx -all-files=True - - -[metadata] -# This includes the license file in the wheel. -license_file = LICENSE.TXT - -[bdist_wheel] -# This flag says to generate wheels that support both Python 2 and Python -# 3. If your code will not run unchanged on both Python 2 and 3, you will -# need to generate separate wheels for each Python version that you -# support. -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index c87787b..0000000 --- a/setup.py +++ /dev/null @@ -1,65 +0,0 @@ -#coding:utf-8 -"""A setuptools based setup module for FDB package. -See: -https://packaging.python.org/en/latest/distributing.html -https://github.com/pypa/sampleproject -""" - -# Always prefer setuptools over distutils -from setuptools import setup, find_packages -# To use a consistent encoding -from codecs import open -from os import path -from fdb import __version__ - -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the README file -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -# Arguments marked as "Required" below must be included for upload to PyPI. -# Fields marked as "Optional" may be commented out. - -setup( - name='fdb', - version=__version__, - description='The Python driver for Firebird', - long_description=long_description, - long_description_content_type='text/x-rst', - url='https://github.com/FirebirdSQL/fdb', - author='Pavel Císař', - author_email='pcisar@users.sourceforge.net', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - - 'Intended Audience :: Developers', - - 'License :: OSI Approved :: BSD License', - - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - - 'Operating System :: POSIX :: Linux', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS', - - 'Topic :: Database', -], - keywords='Firebird', # Optional - packages=find_packages(), # Required - install_requires=['future>=0.16.0'], # Optional - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', - test_suite='nose.collector', - project_urls={ - 'Documentation': 'http://fdb.readthedocs.io/en/latest/', - 'Bug Reports': 'http://tracker.firebirdsql.org/browse/PYFB', - 'Funding': 'https://www.firebirdsql.org/en/donate/', - 'Say Thanks!': 'https://saythanks.io/to/pcisar', - 'Source': 'https://github.com/FirebirdSQL/fdb', - }, -) diff --git a/sphinx/changelog.txt b/sphinx/changelog.txt index 5e5c0f8..6a85c27 100644 --- a/sphinx/changelog.txt +++ b/sphinx/changelog.txt @@ -2,6 +2,17 @@ Changelog ######### +Version 2.0.3 +============= + +Bugs Fixed +---------- + +- #17: Add fb_shutdown_callback and fb_shutdown to internal API +- #3: Function exception_from_status() now properly handles decode errors. +- #114: EventConduit's wait() method never exits on database disconnect +- #117: FDB does not work with Python 3.13 +- Build system changed to Hatch and pyproject.toml Version 2.0.2 ============= diff --git a/test/fbtest.fdb b/tests/fbtest.fdb similarity index 100% rename from test/fbtest.fdb rename to tests/fbtest.fdb diff --git a/test/fbtest20.fdb b/tests/fbtest20.fdb similarity index 100% rename from test/fbtest20.fdb rename to tests/fbtest20.fdb diff --git a/test/fbtest21.fdb b/tests/fbtest21.fdb similarity index 100% rename from test/fbtest21.fdb rename to tests/fbtest21.fdb diff --git a/test/fbtest25.fdb b/tests/fbtest25.fdb similarity index 97% rename from test/fbtest25.fdb rename to tests/fbtest25.fdb index be9c652454cf49399b163e4ef39f5ddcbfda377b..9f849a4058419da21242078552a02486cf714f82 100644 GIT binary patch delta 15674 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 Bz+5s_b=rGe3x#{3` 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 diff --git a/test/fbtest30.fdb b/tests/fbtest30.fdb similarity index 96% rename from test/fbtest30.fdb rename to tests/fbtest30.fdb index 593cc1219c342d9b0a0934087f23a10353286ed8..5e77bdddff039fc1fcef3cc415f344a659f148e6 100644 GIT binary patch delta 41450 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 CA-22# delta 17641 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 C)U$p7 diff --git a/test/gstat25-a.out b/tests/gstat25-a.out similarity index 100% rename from test/gstat25-a.out rename to tests/gstat25-a.out diff --git a/test/gstat25-d.out b/tests/gstat25-d.out similarity index 100% rename from test/gstat25-d.out rename to tests/gstat25-d.out diff --git a/test/gstat25-f.out b/tests/gstat25-f.out similarity index 100% rename from test/gstat25-f.out rename to tests/gstat25-f.out diff --git a/test/gstat25-h.out b/tests/gstat25-h.out similarity index 100% rename from test/gstat25-h.out rename to tests/gstat25-h.out diff --git a/test/gstat25-i.out b/tests/gstat25-i.out similarity index 100% rename from test/gstat25-i.out rename to tests/gstat25-i.out diff --git a/test/gstat25-r.out b/tests/gstat25-r.out similarity index 100% rename from test/gstat25-r.out rename to tests/gstat25-r.out diff --git a/test/gstat25-s.out b/tests/gstat25-s.out similarity index 100% rename from test/gstat25-s.out rename to tests/gstat25-s.out diff --git a/test/gstat30-a.out b/tests/gstat30-a.out similarity index 100% rename from test/gstat30-a.out rename to tests/gstat30-a.out diff --git a/test/gstat30-d.out b/tests/gstat30-d.out similarity index 100% rename from test/gstat30-d.out rename to tests/gstat30-d.out diff --git a/test/gstat30-e.out b/tests/gstat30-e.out similarity index 100% rename from test/gstat30-e.out rename to tests/gstat30-e.out diff --git a/test/gstat30-f.out b/tests/gstat30-f.out similarity index 100% rename from test/gstat30-f.out rename to tests/gstat30-f.out diff --git a/test/gstat30-h.out b/tests/gstat30-h.out similarity index 100% rename from test/gstat30-h.out rename to tests/gstat30-h.out diff --git a/test/gstat30-i.out b/tests/gstat30-i.out similarity index 100% rename from test/gstat30-i.out rename to tests/gstat30-i.out diff --git a/test/gstat30-r.out b/tests/gstat30-r.out similarity index 100% rename from test/gstat30-r.out rename to tests/gstat30-r.out diff --git a/test/gstat30-s.out b/tests/gstat30-s.out similarity index 100% rename from test/gstat30-s.out rename to tests/gstat30-s.out diff --git a/test/testfdb.py b/tests/test_fdb.py similarity index 99% rename from test/testfdb.py rename to tests/test_fdb.py index c8e26d3..f3dfd26 100644 --- a/test/testfdb.py +++ b/tests/test_fdb.py @@ -32,7 +32,8 @@ import sys, os import threading import time -import collections +import collections.abc as collections +from collections import namedtuple from decimal import Decimal from contextlib import closing from re import finditer @@ -131,8 +132,8 @@ def setUp(self): raise Exception("Unsupported Firebird version (%s)" % self.version) # self.cwd = os.getcwd() - self.dbpath = self.cwd if os.path.split(self.cwd)[1] == 'test' \ - else os.path.join(self.cwd, 'test') + self.dbpath = self.cwd if os.path.split(self.cwd)[1] == 'tests' \ + else os.path.join(self.cwd, 'tests') def clear_output(self): self.output.close() self.output = StringIO() @@ -8485,8 +8486,8 @@ def setUp(self): super(TestUtils, self).setUp() self.maxDiff = None def test_objectlist(self): - Item = collections.namedtuple('Item', 'name,size,data') - Point = collections.namedtuple('Point', 'x,y') + Item = namedtuple('Item', 'name,size,data') + Point = namedtuple('Point', 'x,y') data = [Item('A', 100, 'X' * 20), Item('Aaa', 95, 'X' * 50), Item('Abb', 90, 'Y' * 20), From 4aa4ef5f84936efb5224a03210dd0177cf5e914d Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Sat, 3 May 2025 14:46:25 +0200 Subject: [PATCH 21/22] Updated doc requirements --- sphinx/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/requirements.txt b/sphinx/requirements.txt index ab73e38..613891a 100644 --- a/sphinx/requirements.txt +++ b/sphinx/requirements.txt @@ -1,4 +1,4 @@ sphinx-bootstrap-theme>=0.8.1 sphinx-autodoc-typehints>=1.24.0 -sphinx>=5.3 +sphinx==7.2.6 . From dc5d0a67162bb5719a2feecd8272c31fa91f40c0 Mon Sep 17 00:00:00 2001 From: Pavel Cisar Date: Tue, 22 Jul 2025 11:24:40 +0200 Subject: [PATCH 22/22] Fox for #124 --- fdb/fbcore.py | 2 +- pyproject.toml | 2 -- sphinx/changelog.txt | 8 ++++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/fdb/fbcore.py b/fdb/fbcore.py index 575fd16..0d2f2b0 100644 --- a/fdb/fbcore.py +++ b/fdb/fbcore.py @@ -156,7 +156,7 @@ PYTHON_MAJOR_VER = sys.version_info[0] #: Current driver version -__version__ = '2.0.3' +__version__ = '2.0.4' apilevel = '2.0' threadsafety = 1 diff --git a/pyproject.toml b/pyproject.toml index 0110631..e1daa6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,6 @@ classifiers = [ "Topic :: Database", ] dependencies = [ - "firebird-base~=2.0", - "python-dateutil~=2.8", ] [project.urls] diff --git a/sphinx/changelog.txt b/sphinx/changelog.txt index 6a85c27..dda05cd 100644 --- a/sphinx/changelog.txt +++ b/sphinx/changelog.txt @@ -2,6 +2,14 @@ Changelog ######### +Version 2.0.4 +============= + +Bugs Fixed +---------- + +- #124: Dependencies usage + Version 2.0.3 =============