From a9ce975c28d4bcfa2f8a777cfaea2baa40f1703f Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 31 Oct 2022 18:18:58 -0500 Subject: Add inital wrapper --- pyqidx/__init__.py | 5 +++ pyqidx/qidx.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++ pyqidx_build.py | 69 ++++++++++++++++++++++++++++++++++ setup.py | 18 +++++++++ 4 files changed, 200 insertions(+) create mode 100644 pyqidx/__init__.py create mode 100644 pyqidx/qidx.py create mode 100644 pyqidx_build.py create mode 100644 setup.py diff --git a/pyqidx/__init__.py b/pyqidx/__init__.py new file mode 100644 index 0000000..3b515a1 --- /dev/null +++ b/pyqidx/__init__.py @@ -0,0 +1,5 @@ +from pyqidx.qidx import ( + AlignmentRecord, + QidxException, + QueryIndexFile +) diff --git a/pyqidx/qidx.py b/pyqidx/qidx.py new file mode 100644 index 0000000..95f5113 --- /dev/null +++ b/pyqidx/qidx.py @@ -0,0 +1,108 @@ +from pyqidx._qidx import ffi, lib + +from typing import ( + NamedTuple, + Generator, + BinaryIO, + Union, + Optional +) + +class _QidxError( object ): + + code : int + + def __init__( self, code : int ): + self.code = code + + @staticmethod + def from_code( code ): + return _QidxError( code ) + + @property + def message( self ): + _msg = ffi.string( lib.qidx_strerr( self.code ) ) + return str( _msg , encoding='UTF-8' ) + +class QidxException( Exception ): + + @staticmethod + def from_code( err_code ): + return QidxException( _QidxError( err_code ).message ) + +class AlignmentRecord( NamedTuple ): + tid : int + pos : int + vptr : int + +class _Qidx( object ): + + def __init__( self, fd : int ): + self._qidx = ffi.new('qidx_fp_t **') + + err = lib.qidx_open( self._qidx, fd ) + + if err != lib.QIDX_OK: + raise QidxException.from_code( err ) + + def lookup_alignments( self, query_name : str ) \ + -> Generator[AlignmentRecord, None, None]: + qname = ffi.new("char[]", bytes( query_name, encoding="UTF-8" )) + + rec = ffi.new('qidx_record_t **') + err = lib.qidx_lookup_alnrec(self._qidx[0], qname, rec) + + if not rec[0]: + return + + for i in range(rec[0].n_alns): + aln = rec[0].alns[i] + yield AlignmentRecord( aln.tid, aln.pos, aln.vptr ) + + lib.qidx_free_alnrec( rec[0] ) + + def close( self ): + err = lib.qidx_close( self._qidx[0] ) + + if err != lib.QIDX_OK: + raise QidxException.from_code( err ) + +class QueryIndexFile( object ): + + _filename : Optional[ str ] + _file : BinaryIO + _qidx : _Qidx + + def __init__( self, filename_or_file : Union[ BinaryIO, str ] ): + + if isinstance( filename_or_file, str ): + self._filename = filename_or_file + self._file = None + else: + self._file = filename_or_file + self._filename = None + + self._qidx = None + + def open( self ): + if self._filename: + self._file = open( self._filename, 'rb' ) + + self._qidx = _Qidx( self._file.fileno() ) + + def close( self ): + self._qidx.close() + + if self._filename: + self._file.close() + + def __enter__( self ): + self.open() + return self + + def __exit__( self, exc_type, exc_value, tb ): + self.close() + + def lookup_alignments( self, query_name : str ) \ + -> Generator[AlignmentRecord, None, None]: + return (yield from self._qidx.lookup_alignments( query_name )) diff --git a/pyqidx_build.py b/pyqidx_build.py new file mode 100644 index 0000000..1bf8e58 --- /dev/null +++ b/pyqidx_build.py @@ -0,0 +1,69 @@ +from cffi import FFI +ffibuilder = FFI() + +ffibuilder.cdef(""" + // @abstract Structure representing an unique alignment and virtual file pointer + // @field tid chromosome ID + // @field pos 0-based leftmost coordinate + // @field vptr virtual pointer + typedef struct { + uint32_t tid; + uint32_t pos; + uint64_t vptr; + } aln_spec_t; + + // @abstract Structure representing all alignments by queryname + // @field qname queryname (name for the given read) + // @field n_alns number of alignments + // @field alns array with alignments structures + typedef struct { + char * qname; + uint16_t n_alns; + aln_spec_t * alns; + } qidx_record_t; + + typedef enum { + QIDX_OK = 0, + QIDX_ITER_DONE, + QIDX_NO_MEM, + QIDX_NO_BAM_HDR, + QIDX_BAM_READ_FAILURE, + QIDX_REC_ITER_FAILURE, + QIDX_NOT_SORTED, + QIDX_BUCKET_MAX_SIZE_EXCEEDED, + QIDX_MAP_FAIL, + QIDX_IO_FAIL, + QIDX_FAILED_TO_OPEN_BAMFILE, + QIDX_FAILED_TO_OPEN_INDEX_FILE, + QIDX_INVALID_VERSION, + QIDX_INVALID_MAGIC, + } qidx_err_t; + + bool qidx_errno(qidx_err_t err); + + char const * qidx_strerr(qidx_err_t err); + + struct qidx_fp; + typedef struct qidx_fp qidx_fp_t; + + qidx_err_t qidx_open(qidx_fp_t ** fp, int fd); + qidx_err_t qidx_create(qidx_fp_t ** fp, int fd, uint32_t _n_buckets, + uint32_t max_bucket_size); + qidx_err_t qidx_lookup_alnrec(qidx_fp_t * fp, + char const * qname, qidx_record_t ** rec); + void qidx_free_alnrec(qidx_record_t * rec); + qidx_err_t qidx_close(qidx_fp_t * fp); +""") + +ffibuilder.set_source( + "pyqidx._qidx", +""" + #include // the C header of the library +""", + libraries=['qidx'], + extra_link_args=['-Wl,-rpath=/usr/local/lib'], +) # library name, for the linker + + +if __name__ == "__main__": + ffibuilder.compile(verbose=True) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d12a6d8 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup + +import pyqidx_build as pyqidx_build + +setup( + name='pyqidx', + version='0.0.1', + author='Flu0r1ne', + author_email='flu0r1ne@flu0r1ne.net', + description='Python bindings for qidx', + url='https://git.flu0r1ne.net', + license='MIT', + setup_requires=["cffi>=1.0.0"], + cffi_modules=["pyqidx_build.py:ffibuilder"], + install_requires=["cffi>=1.0.0"], + packages=['pyqidx'], + ext_modules=[pyqidx_build.ffibuilder.distutils_extension()], +) -- cgit v1.2.3