Skip to content

Commit

Permalink
[WIP] Started separation of FDB and FDB api classes
Browse files Browse the repository at this point in the history
Due to the poor encapsulation and leakage of ffi functionality into the
FDB python class, FDB and FDBApi classes has been separated to guarantee
a better separation of concerns.

This is work in progress.
  • Loading branch information
tbkr committed Oct 21, 2024
1 parent 410760f commit 44ccf70
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 151 deletions.
238 changes: 238 additions & 0 deletions pyfdb/fdb_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import os
from typing import List

import cffi
import findlibs
from packaging import version

__version__ = "0.0.4"
__fdb_version__ = "5.12.1"

ffi = cffi.FFI()


class FDBException(RuntimeError):
pass


class PatchedLib:
"""
Patch a CFFI library with error handling
Finds the header file associated with the FDB C API and parses it, loads the shared library,
and patches the accessors with automatic python-C error handling.
"""

def __init__(self):
self.path = findlibs.find("fdb5")

if self.path is None:
raise RuntimeError("FDB5 library not found")

ffi.cdef(self.__read_header())
self.__lib = ffi.dlopen(self.path)

# All of the executable members of the CFFI-loaded library are functions in the FDB
# C API. These should be wrapped with the correct error handling. Otherwise forward
# these on directly.

for f in dir(self.__lib):
try:
attr = getattr(self.__lib, f)
setattr(self, f, self.__check_error(attr, f) if callable(attr) else attr)
except Exception as e:
print(e)
print("Error retrieving attribute", f, "from library")

# Initialise the library, and set it up for python-appropriate behaviour

self.fdb_initialise()

# Check the library version

tmp_str = ffi.new("char**")
self.fdb_version(tmp_str)
self.version = ffi.string(tmp_str[0]).decode("utf-8")

if version.parse(self.version) < version.parse(__fdb_version__):
raise RuntimeError(
f"This version of pyfdb ({__version__}) requires fdb version {__fdb_version__} or greater."
f"You have fdb version {self.version} loaded from {self.path}"
)

def __read_header(self):
with open(os.path.join(os.path.dirname(__file__), "processed_fdb.h"), "r") as f:
return f.read()

def __check_error(self, fn, name):
"""
If calls into the FDB library return errors, ensure that they get detected and reported
by throwing an appropriate python exception.
"""

def wrapped_fn(*args, **kwargs):
retval = fn(*args, **kwargs)
if retval != self.__lib.FDB_SUCCESS and retval != self.__lib.FDB_ITERATION_COMPLETE:
error_str = "Error in function {}: {}".format(
name, ffi.string(self.__lib.fdb_error_string(retval)).decode("utf-8", "backslashreplace")
)
raise FDBException(error_str)
return retval

return wrapped_fn

def __repr__(self):
return f"<pyfdb.pyfdb.PatchedLib FDB5 version {self.version} from {self.path}>"


class FDBApi:

@classmethod
def get_gc_fdb_keys(cls):
"""
Create a new `fdb_key_t**` object and return a garbage collected version
of it.
"""
key = ffi.new("fdb_key_t**")
lib.fdb_new_key(key)
# Set free function
return ffi.gc(key[0], lib.fdb_delete_key)

@classmethod
def get_gc_splitkey(cls, splitkey):
"""
Create a new `fdb_splitkey**` object and return a garbage collected version
of it.
"""
lib.fdb_new_splitkey(splitkey)
return ffi.gc(splitkey[0], lib.fdb_delete_splitkey)

@classmethod
def add_fdb_key(cls, fdb_keys, param: str, value: str):
"""
Takes a new `fdb_key_t**` object, a parameter and a value and adds it
as its keys.
param and value are ascii encode during the function call
-----------------------
e.g.
self.__key = FDBApi.get_gc_fdb_keys()
FDBApi.add_fdb_key(self.__key, "param", "value")
"""
lib.fdb_key_add(
fdb_keys,
ffi.new("const char[]", param.encode("ascii")),
ffi.new("const char[]", value.encode("ascii")),
)

@classmethod
def get_gc_fdb_request(cls):
"""
Create a new `fdb_key_t**` object and return a garbage collected version
of it.
"""
newrequest = ffi.new("fdb_request_t**")

# we assume a retrieve request represented as a dictionary
lib.fdb_new_request(newrequest)
return ffi.gc(newrequest[0], lib.fdb_delete_request)

@classmethod
def add_fdb_request(cls, requests, name: str, cvals: List, num_vals: int):
"""
Takes a new `fdb_request_t**` object, a name and a cvals and adds it
as its keys.
param and value are ascii encode during the function call
-----------------------
e.g.
self.__request = FDBApi.get_gc_fdb_request()
FDBApi.add_fdb_request(self.__request, name, cvals, len(values))
"""
lib.fdb_request_add(
requests,
ffi.new("const char[]", name.encode("ascii")),
ffi.new("const char*[]", cvals),
num_vals,
)

@classmethod
def fdb_list(cls, fdb, request, iterator, duplicates):
lib.fdb_list(fdb.ctype, request.ctype, iterator, duplicates)

@classmethod
def fdb_list_no_request(cls, fdb, iterator, duplicates):
lib.fdb_list(fdb.ctype, ffi.NULL, iterator, duplicates)


@classmethod
def register_gc_fdb_iterator(cls, iterator):
return ffi.gc(iterator[0], lib.fdb_delete_listiterator)

@classmethod
def create_ffi_string_ascii_encoded(cls, value):
"""
Create a new const char[] for a given value, encoded as ascii
"""
return ffi.new("const char[]", value.encode("ascii"))

@classmethod
def create_ffi_string_utf8_encoded(cls, value):
"""
Create a new const char[] for a given value, encoded as ascii
"""
return ffi.string(value).decode("utf-8")

@classmethod
def create_ffi_fdb_list_iterator(cls):
"""
Create a new fdb_listiterator_t**
"""
return ffi.new("fdb_listiterator_t**")

@classmethod
def create_ffi_const_char_ptr_ptr(cls):
"""
Create a new const char**
"""
return ffi.new("const char**")

@classmethod
def create_ffi_size_t_ptr(cls):
"""
Create a new const char**
"""
return ffi.new("size_t*")

@classmethod
def create_ffi_fdb_split_key_ptr_ptr(cls):
"""
Create a new fdb_split_key_t** for
"""
return ffi.new("fdb_split_key_t**")

@classmethod
def fdb_expand_request(cls, request):
"""
Forwards call to `PatchedLib::fdb_expand_request` method.
"""
lib.fdb_expand_request(request)

@classmethod
def fdb_iterator_next(cls, iterator):
return lib.fdb_listiterator_next(iterator)

@classmethod
def fdb_listiterator_attrs(cls, iterator, path, off, len):
lib.fdb_listiterator_attrs(iterator, path, off, len)

@classmethod
def fdb_listiterator_splitkey(cls, iterator, key):
lib.fdb_listiterator_splitkey(iterator, key)

@classmethod
def fdb_splitkey_next_metadata(cls, key, k, v, level):
return lib.fdb_splitkey_next_metadata(key, k, v, level)
lib = PatchedLib()
Loading

0 comments on commit 44ccf70

Please sign in to comment.