#!/usr/bin/env python

import os
import re

from glob import glob

from distutils import log
from distutils.errors import CompileError
from setuptools import Command, Distribution
from setuptools_dso import DSO, Extension, setup, build_dso, ProbeToolchain
from epicscorelibs.config import get_config_var
import epicscorelibs.path
import epicscorelibs.version

def pvxsversion():
    with open(os.path.join('configure', 'CONFIG_PVXS_VERSION'), 'r') as F:
        return {M.group(1):M.group(2) for M in re.finditer(r'([A-Z_]+)\s*=\s*(\d+)', F.read())}
pvxsversion = pvxsversion()

def eventversion():
    defs = {}
    with open(os.path.join('bundle', 'libevent', 'cmake','VersionViaGit.cmake'), 'r') as F:
        for M in re.finditer(r'set\s*\(\s*([A-Z_]+)\s+"?([^"\s]+)"?\s*\)', F.read()):
            # there will be multiple definitions, use the first
            defs.setdefault(M.group(1), M.group(2))

    A = defs['EVENT_GIT___VERSION_MAJOR']
    B = defs['EVENT_GIT___VERSION_MINOR']
    C = defs['EVENT_GIT___VERSION_PATCH']
    D = 1 if defs['EVENT_GIT___VERSION_STAGE'].endswith('dev') else 0
    ver = '.'.join((A,B,C))
    full = '%s-%s'%(ver, defs['EVENT_GIT___VERSION_STAGE'])
    num = (int(A)<<24) | (int(B)<<16) | (int(C)<<8) | D

    return {
        'EVENT_VERSION_MAJOR':A,
        'EVENT_VERSION_MINOR':B,
        'EVENT_VERSION_PATCH':C,
        'EVENT_NUMERIC_VERSION':'0x%08x'%num,
        'EVENT_PACKAGE_VERSION':ver,
        'EVENT_VERSION':full,
    }
eventversion = eventversion()

def cexpand(iname, oname, defs={}, dry_run=False):
    """Expand input file to output file.
    
    defs dict used to expand "@MACRO@", or replace "#cmakedefine MACRO VALUE" lines
    """
    log.info('expand %s -> %s', iname, oname)
    log.debug('With %s', defs)
    with open(iname, 'r') as F:
        inp = F.read()

    def msub(M):
        ret= defs[M.group(1)]
        assert isinstance(ret, str), (M.group(1), ret)
        return ret
    out = re.sub(r'@([^@]+)@', msub, inp)

    def csub(M):
        act = defs[M.group(1)]
        if act is None:
            return '/* #undef %s */'%M.group(1)
        elif M.lastindex>1:
            return '#define %s %s'%(M.group(1), M.group(2))
        else:
            return '#define %s'%(M.group(1))
    out = re.sub(r'^\s*#\s*cmakedefine\s+([^\s]+)(?:\s+(.*))?$', csub, out, 0, re.MULTILINE)

    if dry_run:
        log.info('Would write %s', oname)
    else:
        log.info('Write %s', oname)
        log.info('>>>>>>>>')
        log.info('%s', out)
        log.info('<<<<<<<<')
        with open(oname, 'w') as F:
            F.write(out)

def logexc(fn):
    def wrapit(*args, **kws):
        try:
            return fn(*args, **kws)
        except:
            import traceback
            traceback.print_exc()
            raise
    return wrapit

class Expand(Command):
    user_options = [
        ('build-lib=', 't',
         "directory for temporary files (build by-products)"),
        ('build-temp=', 't',
         "directory for temporary files (build by-products)"),
    ]
    def initialize_options(self):
        self.build_lib = None
        self.build_temp = None

    def finalize_options(self):
        self.set_undefined_options('build',
                                   ('build_lib', 'build_lib'),
                                   ('build_temp', 'build_temp'),
                                  )

    @logexc
    def run(self):
        log.info("In Expand")
        self.mkpath(os.path.join(self.build_temp, 'event2'))
        self.mkpath(os.path.join(self.build_lib, 'pvxslibs', 'include', 'pvxs'))

        OS_CLASS = get_config_var('OS_CLASS')

        self.distribution.DEFS = DEFS = {
            # *NIX unsupported by EPICS Base
            '_ALL_SOURCE':None,
            '_TANDEM_SOURCE':None,
            '__EXTENSIONS__':None,
            '_MINIX':None,
            '__EXT_POSIX2':None,
            '_LARGE_FILES':None,
            '_POSIX_PTHREAD_SEMANTICS':None,
            'EVENT__HAVE_SA_FAMILY_T':None,
            # unused
            '_FILE_OFFSET_BITS':None,
            '_POSIX_1_SOURCE':None,
            '_POSIX_SOURCE':None,
            # config
            'EVENT__DISABLE_DEBUG_MODE':None,
            'EVENT__DISABLE_MM_REPLACEMENT':None,
            'EVENT__DISABLE_THREAD_SUPPORT':None,
            'EVENT__HAVE_LIBZ':None,
            'EVENT__HAVE_OPENSSL':None,
            'EVENT__HAVE_MBEDTLS':None,
        }

        DEFS.update(pvxsversion) # PVXS_*_VERSION
        DEFS.update(eventversion) # EVENT*VERSION

        for var in ('EPICS_HOST_ARCH', 'T_A', 'OS_CLASS', 'CMPLR_CLASS'):
            DEFS[var] = get_config_var(var)

        probe = ProbeToolchain()

        if probe.check_symbol('__GNU_LIBRARY__', headers=['features.h']):
            DEFS['_GNU_SOURCE'] = '1'
            probe.define_macros += [('_GNU_SOURCE', None)]
        else:
            DEFS['_GNU_SOURCE'] = None

        if OS_CLASS=='WIN32':
            probe.headers += ['winsock2.h', 'ws2tcpip.h']
            probe.define_macros += [('Iwinsock2.h', None), ('Iws2tcpip.h', None), ('_WIN32_WINNT', '0x0600')]

            if probe.check_include('afunix.h'):
                DEFS['EVENT__HAVE_AFUNIX_H'] = '1'
                probe.headers.append('afunix.h')
            else:
                DEFS['EVENT__HAVE_AFUNIX_H'] = None

            DEFS.update({
                'EVENT__HAVE_INET_NTOP':'0',
                'EVENT__HAVE_INET_PTON':'0',
                'EVENT__HAVE_PTHREADS':'0',
                'EVENT__HAVE_WEPOLL':'1',
            })

        else:
            DEFS.update({
                'EVENT__HAVE_AFUNIX_H':None,
                'EVENT__HAVE_PTHREADS':'1',
                'EVENT__HAVE_WEPOLL':None,
            })
            probe.headers += ['pthread.h']

        for hfile, isextra in [('sys/types.h', True),
                            ('sys/socket.h', True),
                            ('sys/random.h', True),
                            ('netinet/in.h', True),
                            ('sys/un.h', True),
                            ('netinet/in6.h', True),
                            ('unistd.h', False),
                            ('netdb.h', False),
                            ('dlfcn.h', False),
                            ('arpa/inet.h', False),
                            ('fcntl.h', True),
                            ('inttypes.h', False),
                            ('memory.h', False),
                            ('poll.h', False),
                            ('port.h', True),
                            ('signal.h', False),
                            ('stdarg.h', False),
                            ('stddef.h', False),
                            ('stdint.h', False),
                            ('stdlib.h', False),
                            ('strings.h', False),
                            ('string.h', False),
                            ('sys/devpoll.h', False),
                            ('sys/epoll.h', False),
                            ('sys/eventfd.h', False),
                            ('sys/event.h', False),
                            ('sys/ioctl.h', False),
                            ('sys/mman.h', False),
                            ('sys/param.h', False),
                            ('sys/queue.h', False),
                            ('sys/select.h', False),
                            ('sys/sendfile.h', False),
                            ('sys/stat.h', False),
                            ('sys/time.h', True),
                            ('sys/uio.h', False),
                            ('sys/types.h', True),
                            ('ifaddrs.h', False),
                            ('mach/mach_time.h', False),
                            ('mach/mach.h', False),
                            ('netinet/tcp.h', False),
                            ('sys/wait.h', False),
                            ('sys/resource.h', False),
                            ('sys/sysctl.h', False), # TODO !linux
                            ('sys/timerfd.h', False),
                            ('errno.h', False)]:
            if probe.check_include(hfile):
                DEFS['EVENT__HAVE_'+hfile.upper().replace('/','_').replace('.','_')] = '1'
                probe.headers.append(hfile)
            else:
                DEFS['EVENT__HAVE_'+hfile.upper().replace('/','_').replace('.','_')] = None

        for sym in ['epoll_create',
                'epoll_ctl',
                'eventfd',
                'clock_gettime',
                'fcntl',
                'gettimeofday',
                'kqueue',
                'mmap',
                'pipe',
                'pipe2',
                'poll',
                'port_create',
                'sendfile',
                'sigaction',
                'signal',
                'strsignal',
                'splice',
                'strlcpy',
                'strsep',
                'strtok_r',
                'vasprintf',
                'sysctl',
                'accept4',
                'arc4random',
                'arc4random_buf',
                'arc4random_addrandom',
                'epoll_create1',
                'getegid',
                'geteuid',
                'getifaddrs',
                'issetugid',
                'mach_absolute_time',
                'nanosleep',
                'usleep',
                'timeradd',
                'timerfd_create',
                'pthread_mutexattr_setprotocol',
                'setenv',
                'setrlimit',
                'umask',
                'unsetenv',
                'gethostbyname_r',
                'getservbyname',
                'select',
                '_gmtime64_s',
                '_gmtime64',
                '__FUNCTION__',
                'F_SETFD',
                'getrandom',
                'getaddrinfo',
                'getnameinfo',
                'getprotobynumber',
                'inet_ntop',
                'inet_pton',
                'strtoll',
                'timerclear',
                'timercmp',
                'timerisset',
                'putenv']:
            DEFS['EVENT__HAVE_'+sym.upper().replace('/','_').replace('.','_')] = '1' if probe.check_symbol(sym) else None

        DEFS['EVENT__HAVE___func__'] = '1' if probe.check_symbol('__func__') else None

        DEFS['EVENT__HAVE_EPOLL'] = DEFS['EVENT__HAVE_EPOLL_CREATE']
        DEFS['EVENT__HAVE_DEVPOLL'] = DEFS['EVENT__HAVE_SYS_DEVPOLL_H']

        DEFS['EVENT__HAVE_TAILQFOREACH'] = '1' if probe.check_symbol('TAILQ_FOREACH', ['sys/queue.h']) else None
        DEFS['EVENT__HAVE_DECL_CTL_KERN'] = '1' if probe.check_symbol('CTL_KERN', ['sys/sysctl.h']) else '0'
        DEFS['EVENT__HAVE_DECL_KERN_ARND'] = '1' if probe.check_symbol('KERN_ARND', ['sys/sysctl.h']) else '0'
        DEFS['EVENT__HAVE_FD_MASK'] = '1' if probe.check_symbol('FD_MASK') else '0'
        DEFS['EVENT__HAVE_SETFD'] = '1' if probe.check_symbol('F_SETFD') else '0'

        DEFS['EVENT__DNS_USE_CPU_CLOCK_FOR_ID'] = DEFS['EVENT__HAVE_CLOCK_GETTIME']

        DEFS['EVENT__DNS_USE_GETTIMEOFDAY_FOR_ID'] = None
        DEFS['EVENT__DNS_USE_FTIME_FOR_ID'] = None

        DEFS['EVENT__HAVE_EVENT_PORTS'] = '1' if DEFS['EVENT__HAVE_PORT_H']=='1' and DEFS['EVENT__HAVE_PORT_CREATE']=='1' else None

        if OS_CLASS=='WIN32':
            # windows has select(), but support comes from win32select.c instead of the usual select.c
            DEFS['EVENT__HAVE_SELECT'] = None

        for kw in ['inline', 'size_t']:
            DEFS['EVENT__'+kw] = kw
        DEFS['EVENT__inline'] = 'inline'
        DEFS['EVENT__size_t'] = 'size_t'

        DEFS['EVENT__HAVE_GETHOSTBYNAME_R_3_ARG'] = '1' if probe.try_compile('''
            #include <netdb.h>
            int wrap_gethostbyname_r(const char *name, struct hostent *hp, struct hostent_data *hdata) {
                return gethostbyname_r(name, hp, hdata);
            }
            ''') else None

        DEFS['EVENT__HAVE_GETHOSTBYNAME_R_5_ARG'] = '1' if probe.try_compile('''
            #include <netdb.h>
            struct hostent *wrap_gethostbyname_r(const char *name, struct hostent *hp, char *buf, size_t buflen, int *herr) {
                return gethostbyname_r(name, hp, buf, buflen, herr);
            }
            ''') else None

        DEFS['EVENT__HAVE_GETHOSTBYNAME_R_6_ARG'] = '1' if probe.try_compile('''
            #include <netdb.h>
            int wrap_gethostbyname_r(const char *name, struct hostent *hp, char *buf, size_t buflen, struct hostent **result, int *herr) {
                return gethostbyname_r(name, hp, buf, buflen, result, herr);
            }
            ''') else None

        for type in ['uint8_t',
                    'uint16_t',
                    'uint32_t',
                    'uint64_t',
                    'short',
                    'int',
                    'unsigned',
                    'unsigned',
                    'unsigned int',
                    'long',
                    'long long',
                    'void *',
                    'pthread_t',
                    'uintptr_t',
                    'size_t',
                    #'ssize_t',
                    #'SSIZE_T',
                    'off_t',
                    'socklen_t',
                    'pid_t',
                    'sa_family_t',
                    'struct addrinfo',
                    'struct in6_addr',
                    'struct sockaddr_in',
                    'struct sockaddr_in6',
                    'struct sockaddr_un',
                    'struct sockaddr_storage',
                    'time_t',
                    'struct linger']:
            mangled_type = type.upper().replace(' ','_').replace('*', 'P')
            try:
                DEFS['EVENT__SIZEOF_'+mangled_type] = str(probe.sizeof(type))
            except CompileError:
                DEFS['EVENT__SIZEOF_'+mangled_type] = '0'
                DEFS['EVENT__HAVE_'+mangled_type] = None
            else:
                DEFS['EVENT__HAVE_'+mangled_type] = '1'

        try:
            DEFS['EVENT__SIZEOF_SSIZE_T'] = str(probe.sizeof('ssize_t'))
            DEFS['EVENT__ssize_t'] = 'ssize_t'
        except CompileError:
            DEFS['EVENT__SIZEOF_SSIZE_T'] = str(probe.sizeof('SSIZE_T'))
            DEFS['EVENT__ssize_t'] = 'SSIZE_T'
            # libevent CMakeLists.txt defaults to 'int' if neither is available, which seems wrong...
        DEFS['EVENT__HAVE_SSIZE_T'] = '1'

        if DEFS['EVENT__SIZEOF_SOCKLEN_T']!='0':
            DEFS['EVENT__socklen_t'] = 'socklen_t'

        else:
            DEFS['EVENT__socklen_t'] = 'unsigned int'
            DEFS['EVENT__SIZEOF_SOCKLEN_T'] = DEFS['EVENT__SIZEOF_UNSIGNED_INT']

        for struct, member in [('struct in6_addr', 's6_addr16'),
                               ('struct in6_addr', 's6_addr32'),
                               ('struct sockaddr_in6', 'sin6_len'),
                               ('struct sockaddr_in', 'sin_len'),
                               ('struct sockaddr_storage', 'ss_family'),
                               ('struct sockaddr_storage', '__ss_family')]:
            mangled_mem = ('EVENT__HAVE_%s_%s'%(struct, member)).upper().replace(' ','_')
            DEFS[mangled_mem] = '1' if probe.check_member(struct, member) else None

        DEFS['EVENT__TIME_WITH_SYS_TIME'] = None

        # TODO: assume working kqueue
        DEFS['EVENT__HAVE_WORKING_KQUEUE'] = DEFS['EVENT__HAVE_KQUEUE']
        print(DEFS)


        cexpand('src/describe.h@',
                os.path.join(self.build_temp, 'describe.h'),
                DEFS, dry_run=self.dry_run)
        cexpand('src/pvxs/versionNum.h@',
                os.path.join(self.build_lib, 'pvxslibs', 'include', 'pvxs', 'versionNum.h'),
                DEFS, dry_run=self.dry_run)
        cexpand('bundle/libevent/evconfig-private.h.cmake',
                os.path.join(self.build_temp, 'evconfig-private.h'),
                DEFS, dry_run=self.dry_run)
        cexpand('bundle/libevent/event-config.h.cmake',
                os.path.join(self.build_temp, 'event2', 'event-config.h'),
                DEFS, dry_run=self.dry_run)

        if self.dry_run:
            log.info('Would create pvxsVCS.h')
        else:
            log.info('Writing pvxsVCS.h')
            with open(os.path.join(self.build_temp, 'pvxsVCS.h'), 'w') as F:
                F.write('''
#ifndef PVXS_VCS_VERSION
#  define PVXS_VCS_VERSION "pip"
#endif 
''')

class InstallHeaders(Command):
    user_options = [
        ('build-lib=', 't',
         "directory for temporary files (build by-products)"),
        ('build-temp=', 't',
         "directory for temporary files (build by-products)"),
    ]
    def initialize_options(self):
        self.build_lib = None
        self.build_temp = None

    def finalize_options(self):
        self.set_undefined_options('build',
                                   ('build_lib', 'build_lib'),
                                   ('build_temp', 'build_temp'),
                                  )
    def run(self):
        log.info("In InstallHeaders")
        self.mkpath(os.path.join(self.build_lib, 'pvxs'))

        for header in glob('src/pvxs/*.h'):
            self.copy_file(header,
                           os.path.join(self.build_lib, 'pvxslibs', 'include', os.path.relpath(header, 'src')))


@logexc
def define_DSOS(self):
    DEFS = self.distribution.DEFS
    OS_CLASS = get_config_var('OS_CLASS')

    src_core = [
        'buffer.c',
        'bufferevent.c',
        'bufferevent_filter.c',
        'bufferevent_pair.c',
        'bufferevent_ratelim.c',
        'bufferevent_sock.c',
        'event.c',
        'evmap.c',
        'evthread.c',
        'evutil.c',
        'evutil_rand.c',
        'evutil_time.c',
        'watch.c',
        'listener.c',
        'log.c',
        'signal.c',
        'strlcpy.c',
    ]

    if DEFS['EVENT__HAVE_POLL']=='1':
        src_core += ['poll.c']

    if DEFS['EVENT__HAVE_KQUEUE']=='1':
        src_core += ['kqueue.c']

    if DEFS['EVENT__HAVE_DEVPOLL']=='1':
        src_core += ['devpoll.c']

    if DEFS['EVENT__HAVE_WEPOLL']=='1':
        src_core += ['wepoll.c', 'epoll.c']
    elif DEFS['EVENT__HAVE_EPOLL']=='1':
        src_core += ['epoll.c']

    if DEFS['EVENT__HAVE_EVENT_PORTS']=='1':
        src_core += ['evport.c']

    if OS_CLASS=='WIN32':
        src_core += [
            'buffer_iocp.c',
            'bufferevent_async.c',
            'event_iocp.c',
            'win32select.c',
            'evthread_win32.c',
        ]
    elif DEFS['EVENT__HAVE_SELECT']=='1':
        src_core += ['select.c']

    src_core = [os.path.join('bundle', 'libevent', src) for src in src_core]

    src_pvxs = [
        'describe.cpp',
        'log.cpp',
        'unittest.cpp',
        'util.cpp',
        'osgroups.cpp',
        'sharedarray.cpp',
        'bitmask.cpp',
        'type.cpp',
        'data.cpp',
        'datafmt.cpp',
        'pvrequest.cpp',
        'dataencode.cpp',
        'nt.cpp',
        'evhelper.cpp',
        'udp_collector.cpp',
        'config.cpp',
        'conn.cpp',
        'server.cpp',
        'serverconn.cpp',
        'serverchan.cpp',
        'serverintrospect.cpp',
        'serverget.cpp',
        'servermon.cpp',
        'serversource.cpp',
        'sharedpv.cpp',
        'client.cpp',
        'clientreq.cpp',
        'clientconn.cpp',
        'clientintrospect.cpp',
        'clientget.cpp',
        'clientmon.cpp',
        'clientdiscover.cpp',
    ]

    src_pvxs = [os.path.join('src', src) for src in src_pvxs]

    if OS_CLASS=='WIN32':
        src_pvxs += ['src/os/WIN32/osdSockExt.cpp']
    else:
        src_pvxs += ['src/os/default/osdSockExt.cpp']

    event_libs = []
    if OS_CLASS=='WIN32':
        event_libs = ['ws2_32','shell32','advapi32','bcrypt','iphlpapi']

    probe = ProbeToolchain()

    cxx11_flags = []
    if probe.try_compile('int probefn() { auto x=1; return x; }',
                         language='c++',
                         extra_preargs=['-std=c++11']):
        cxx11_flags += ['-std=c++11']

    pvxs_abi = '%(PVXS_MAJOR_VERSION)s.%(PVXS_MINOR_VERSION)s'%pvxsversion
    event_abi = '%(EVENT_VERSION_MAJOR)s.%(EVENT_VERSION_MINOR)s.%(EVENT_VERSION_PATCH)s'%eventversion

    DSOS = []

    dsos_pvxs = [
        'epicscorelibs.lib.Com',
        'pvxslibs.lib.event_core',
    ]

    if OS_CLASS!='WIN32':
        DSOS += [DSO('pvxslibs.lib.event_pthread',
                [os.path.join('bundle', 'libevent', 'evthread_pthread.c')],
                define_macros = [('event_pthreads_shared_EXPORTS', None)],
                include_dirs=[
                    'bundle/libevent/include',
                    'bundle/libevent/compat', # for sys/queue.h
                    '.'
                ],
                soversion = event_abi,
        )]
        dsos_pvxs += ['pvxslibs.lib.event_pthread']

    DSOS += [
        DSO('pvxslibs.lib.event_core', src_core,
            define_macros = [('event_core_shared_EXPORTS', None)],
            include_dirs=[
                'bundle/libevent/include',
                'bundle/libevent/compat', # for sys/queue.h
                '.'
            ],
            soversion = event_abi,
            libraries = event_libs,
        ),
        DSO('pvxslibs.lib.pvxs', src_pvxs,
            define_macros = [('PVXS_API_BUILDING', None), ('PVXS_ENABLE_EXPERT_API', None)] + get_config_var('CPPFLAGS'),
            include_dirs=[
                'bundle/libevent/include',
                'src',
                '.', # generated headers under build/tmp
                'pvxslibs/include', # generated headers under build/lib
                epicscorelibs.path.include_path
                ],
            extra_compile_args = cxx11_flags + get_config_var('CXXFLAGS'),
            extra_link_args = cxx11_flags + get_config_var('LDFLAGS'),
            soversion = pvxs_abi,
            dsos = dsos_pvxs,
            libraries = get_config_var('LDADD') + event_libs,
        )
    ]

    return DSOS

build_dso.sub_commands.extend([
    ('build_expand', lambda self:True),
    ('install_epics_headers', lambda self:True),
])


pvxs_ver = '%(PVXS_MAJOR_VERSION)s.%(PVXS_MINOR_VERSION)s.%(PVXS_MAINTENANCE_VERSION)s'%pvxsversion
#pvxs_ver += 'a1'

with open(os.path.join(os.path.dirname(__file__), 'README.md')) as F:
    long_description = F.read()

setup(
    name='pvxslibs',
    version=pvxs_ver,
    description="PVXS libraries packaged for python",
    long_description=long_description,
    long_description_content_type='text/markdown',
    url='https://mdavidsaver.github.io/pvxs',
    author='Michael Davidsaver',
    author_email='mdavidsaver@gmail.com',
    license='BSD',
    classifiers = [
        'Development Status :: 5 - Production/Stable',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: Implementation :: CPython',
        'License :: OSI Approved :: BSD License',
        'Intended Audience :: Science/Research',
        'Topic :: Scientific/Engineering',
        'Topic :: Software Development :: Libraries',
        'Topic :: System :: Distributed Computing',
        'Operating System :: POSIX :: Linux',
        'Operating System :: MacOS',
        'Operating System :: Microsoft :: Windows',
    ],
    keywords='epics scada',
    python_requires='>=2.7',

    # setup/build time dependencies listed in pyproject.toml
    # cf. PEP 518
    #setup_requires = ['setuptools_dso'],
    # also need at runtime for DSO filename lookup
    install_requires = [
        'setuptools_dso>=2.7a1',
        epicscorelibs.version.abi_requires(),
    ],
    packages=['pvxslibs', 'pvxslibs.lib', 'pvxslibs.test'],
    package_dir={'': 'python'},
    x_dsos = define_DSOS,
    cmdclass = {
        'build_expand': Expand,
        'install_epics_headers':InstallHeaders,
    },
    zip_safe = False
)
