diff --git a/tests/res/config.ini b/tests/res/config.ini
deleted file mode 100644
index a8740ac..0000000
--- a/tests/res/config.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[General]
-logfile = tests/res/test.log
-outputfile = tests/res/output.html
diff --git a/tsstats.py b/tsstats.py
deleted file mode 100755
index 87adf6d..0000000
--- a/tsstats.py
+++ /dev/null
@@ -1,300 +0,0 @@
-import argparse
-import configparser
-import datetime
-import glob
-import json
-import logging
-import re
-from os import sep
-from os.path import exists
-from time import localtime, strftime
-
-from jinja2 import Environment, FileSystemLoader
-
-
-class Exceptions:
-    class ConfigNotFound(Exception):
-        pass
-
-    class InvalidConfig(Exception):
-        pass
-
-    class InvalidLog(Exception):
-        pass
-
-exceptions = Exceptions
-
-
-class Clients:
-
-    def __init__(self, ident_map={}):
-        self.clients_by_id = {}
-        self.clients_by_uid = {}
-        self.ident_map = ident_map
-
-    def is_id(self, id_or_uid):
-        try:
-            int(id_or_uid)
-            return True
-        except ValueError:
-            return False
-
-    def __add__(self, id_or_uid):
-        if self.is_id(id_or_uid):
-            if id_or_uid not in self.clients_by_id:
-                self.clients_by_id[id_or_uid] = Client(id_or_uid)
-        else:
-            if id_or_uid not in self.clients_by_uid:
-                self.clients_by_uid[id_or_uid] = Client(id_or_uid)
-        return self
-
-    def __getitem__(self, id_or_uid):
-        if id_or_uid in self.ident_map:
-            id_or_uid = self.ident_map[id_or_uid]
-        if self.is_id(id_or_uid):
-            if id_or_uid not in self.clients_by_id:
-                self += id_or_uid
-            return self.clients_by_id[id_or_uid]
-        else:
-            if id_or_uid not in self.clients_by_uid:
-                self += id_or_uid
-            return self.clients_by_uid[id_or_uid]
-
-    def __iter__(self):
-        for id_client in self.clients_by_id.values():
-            yield id_client
-        for uid_client in self.clients_by_uid.values():
-            yield uid_client
-
-
-class Client:
-
-    def __init__(self, identifier):
-        # public
-        self.identifier = identifier
-        self.nick = None
-        self.connected = 0
-        self.onlinetime = 0
-        self.kicks = 0
-        self.pkicks = 0
-        self.bans = 0
-        self.pbans = 0
-        self.last_seen = 0
-        # private
-        self._last_connect = 0
-
-    def connect(self, timestamp):
-        '''
-        client connects at "timestamp"
-        '''
-        logging.debug('CONNECT {}'.format(str(self)))
-        self.connected += 1
-        self._last_connect = timestamp
-
-    def disconnect(self, timestamp):
-        '''
-        client disconnects at "timestamp"
-        '''
-        logging.debug('DISCONNECT {}'.format(str(self)))
-        if not self.connected:
-            logging.debug('^ disconnect before connect')
-            raise exceptions.InvalidLog('disconnect before connect!')
-        self.connected -= 1
-        session_time = timestamp - self._last_connect
-        self.onlinetime += session_time
-        self.last_seen = timestamp
-
-    def kick(self, target):
-        '''
-        client kicks "target" (Client-obj)
-        '''
-        logging.debug('KICK {} -> {}'.format(str(self), str(target)))
-        target.pkicks += 1
-        self.kicks += 1
-
-    def ban(self, target):
-        '''
-        client bans "target" (Client-obj)
-        '''
-        logging.debug('BAN {} -> {}'.format(str(self), str(target)))
-        target.pbans += 1
-        self.bans += 1
-
-    def __str__(self):
-        return '<{},{}>'.format(self.identifier, self.nick)
-
-    def __getitem__(self, item):
-        return {
-            'identifier': self.identifier,
-            'nick': self.nick,
-            'connected': self.connected,
-            'onlinetime': self.onlinetime,
-            'kicks': self.kicks,
-            'pkicks': self.pkicks,
-            'bans': self.bans,
-            'pbans': self.pbans,
-        }[item]
-
-
-re_dis_connect = re.compile(r"'(.*)'\(id:(\d*)\)")
-re_disconnect_invoker = re.compile(
-    r"invokername=(.*)\ invokeruid=(.*)\ reasonmsg"
-)
-path_split = __file__.split(sep)[:-1]
-abspath = sep.join(path_split)
-if len(path_split) > 0:
-    abspath += sep
-
-
-def gen_abspath(filename):
-    return filename if filename.startswith(sep) else abspath + filename
-
-
-def _get_sorted(stor, key):
-    clients = stor.values()
-    cl_data = [(client, client[key]) for client in clients if client[key] > 0]
-    return sorted(cl_data, key=lambda data: data[1], reverse=True)
-
-
-def _format_seconds(seconds):
-    minutes, seconds = divmod(seconds, 60)
-    hours, minutes = divmod(minutes, 60)
-    hours = str(hours) + 'h ' if hours > 0 else ''
-    minutes = str(minutes) + 'm ' if minutes > 0 else ''
-    seconds = str(seconds) + 's' if seconds > 0 else ''
-    return hours + minutes + seconds
-
-
-def parse_logs(log_path, ident_map={}, file_log=False):
-    clients = Clients(ident_map)
-    # setup logging
-    log = logging.getLogger()
-    log.setLevel(logging.DEBUG)
-    if file_log:
-        # file logger
-        file_handler = logging.FileHandler('debug.txt', 'w', 'UTF-8')
-        file_handler.setFormatter(logging.Formatter('%(message)s'))
-        file_handler.setLevel(logging.DEBUG)
-        log.addHandler(file_handler)
-    # stream logger (unused)
-    stream_handler = logging.StreamHandler()
-    stream_handler.setLevel(logging.INFO)
-    log.addHandler(stream_handler)
-
-    # find all log-files and open them
-    file_paths = sorted([file_path for file_path in glob.glob(log_path)])
-
-    for file_path in file_paths:
-        log_file = open(file_path)
-        # process lines
-        logging.debug('Started parsing of {}'.format(log_file.name))
-        for line in log_file:
-            parts = line.split('|')
-            log_format = '%Y-%m-%d %H:%M:%S.%f'
-            stripped_time = datetime.datetime.strptime(parts[0], log_format)
-            logdatetime = int(stripped_time.timestamp())
-            data = '|'.join(parts[4:]).strip()
-            if data.startswith('client'):
-                nick, clid = re_dis_connect.findall(data)[0]
-                if data.startswith('client connected'):
-                    client = clients[clid]
-                    client.nick = nick
-                    client.connect(logdatetime)
-                elif data.startswith('client disconnected'):
-                    client = clients[clid]
-                    client.nick = nick
-                    client.disconnect(logdatetime)
-                    if 'invokeruid' in data:
-                        re_disconnect_data = re_disconnect_invoker.findall(
-                            data)
-                        invokernick, invokeruid = re_disconnect_data[0]
-                        invoker = clients[invokeruid]
-                        invoker.nick = invokernick
-                        if 'bantime' in data:
-                            invoker.ban(client)
-                        else:
-                            invoker.kick(client)
-        logging.debug('Finished parsing of {}'.format(log_file.name))
-    return clients
-
-
-def render_template(clients, output, template_name='template.html',
-                    title='TeamspeakStats', debug=False):
-    # prepare clients
-    clients_onlinetime_ = _get_sorted(clients.clients_by_id, 'onlinetime')
-    clients_onlinetime = [
-        (client, _format_seconds(onlinetime))
-        for client, onlinetime in clients_onlinetime_
-    ]
-
-    clients_kicks = _get_sorted(clients.clients_by_uid, 'kicks')
-    clients_pkicks = _get_sorted(clients.clients_by_id, 'pkicks')
-    clients_bans = _get_sorted(clients.clients_by_uid, 'bans')
-    clients_pbans = _get_sorted(clients.clients_by_id, 'pbans')
-    objs = [('Onlinetime', clients_onlinetime), ('Kicks', clients_kicks),
-            ('passive Kicks', clients_pkicks),
-            ('Bans', clients_bans), ('passive Bans', clients_pbans)]
-
-    # render
-    template_loader = FileSystemLoader(abspath)
-    template_env = Environment(loader=template_loader)
-
-    def frmttime(timestamp):
-        return strftime('%x %X', localtime(int(timestamp)))
-    template_env.filters['frmttime'] = frmttime
-    template = template_env.get_template(template_name)
-    with open(output, 'w') as f:
-        f.write(template.render(title=title, objs=objs, debug=debug))
-
-
-def parse_config(config_path):
-    config = configparser.ConfigParser()
-    config.read(config_path)
-    if 'General' not in config or not \
-            ('logfile' in config['General'] and
-                'outputfile' in config['General']):
-        raise exceptions.InvalidConfig
-
-    general = config['General']
-    log_path = gen_abspath(general['logfile'])
-    output_path = gen_abspath(general['outputfile'])
-    return log_path, output_path
-
-
-def main(config_path='config.ini', id_map_path='id_map.json',
-         debug=False, debugfile=False):
-    # check cmdline-args
-    config_path = gen_abspath(config_path)
-    id_map_path = gen_abspath(id_map_path)
-
-    if not exists(config_path):
-        raise exceptions.ConfigNotFound(config_path)
-
-    if exists(id_map_path):
-        # read id_map
-        id_map = json.load(open(id_map_path))
-    else:
-        id_map = {}
-
-    log_path, output_path = parse_config(config_path)
-    clients = parse_logs(log_path, ident_map=id_map, file_log=debugfile)
-    render_template(clients, output=output_path, debug=debug)
-
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser(
-        description='A simple Teamspeak stats-generator - based on server-logs'
-    )
-    parser.add_argument(
-        '--config', type=str, help='path to config', default='config.ini'
-    )
-    parser.add_argument(
-        '--idmap', type=str, help='path to id_map', default='id_map.json'
-    )
-    parser.add_argument(
-        '--debug', help='debug mode', action='store_true'
-    )
-    parser.add_argument(
-        '--debugfile', help='write debug-log to file', action='store_true'
-    )
-    args = parser.parse_args()
-    main(args.config, args.idmap, args.debug, args.debugfile)
diff --git a/tsstats/__init__.py b/tsstats/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tsstats/__main__.py b/tsstats/__main__.py
new file mode 100644
index 0000000..166de14
--- /dev/null
+++ b/tsstats/__main__.py
@@ -0,0 +1,48 @@
+import argparse
+import json
+from os.path import abspath, exists
+
+from tsstats.config import parse_config
+from tsstats.exceptions import ConfigNotFound
+from tsstats.log import parse_logs
+from tsstats.template import render_template
+
+
+def main(config_path='config.ini', id_map_path='id_map.json',
+         debug=False, debugfile=False):
+    # check cmdline-args
+    config_path = abspath(config_path)
+    id_map_path = abspath(id_map_path)
+
+    if not exists(config_path):
+        raise ConfigNotFound(config_path)
+
+    if exists(id_map_path):
+        # read id_map
+        id_map = json.load(open(id_map_path))
+    else:
+        id_map = {}
+
+    log_path, output_path = parse_config(config_path)
+    clients = parse_logs(log_path, ident_map=id_map, file_log=debugfile)
+    render_template(clients, output=output_path, debug=debug)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description='A simple Teamspeak stats-generator - based on server-logs'
+    )
+    parser.add_argument(
+        '--config', type=str, help='path to config', default='config.ini'
+    )
+    parser.add_argument(
+        '--idmap', type=str, help='path to id_map', default='id_map.json'
+    )
+    parser.add_argument(
+        '--debug', help='debug mode', action='store_true'
+    )
+    parser.add_argument(
+        '--debugfile', help='write debug-log to file', action='store_true'
+    )
+    args = parser.parse_args()
+    main(args.config, args.idmap, args.debug, args.debugfile)
diff --git a/tsstats/client.py b/tsstats/client.py
new file mode 100644
index 0000000..8ece944
--- /dev/null
+++ b/tsstats/client.py
@@ -0,0 +1,114 @@
+import logging
+
+from tsstats.exceptions import InvalidLog
+
+
+class Clients:
+
+    def __init__(self, ident_map={}):
+        self.clients_by_id = {}
+        self.clients_by_uid = {}
+        self.ident_map = ident_map
+
+    def is_id(self, id_or_uid):
+        try:
+            int(id_or_uid)
+            return True
+        except ValueError:
+            return False
+
+    def __add__(self, id_or_uid):
+        if self.is_id(id_or_uid):
+            if id_or_uid not in self.clients_by_id:
+                self.clients_by_id[id_or_uid] = Client(id_or_uid)
+        else:
+            if id_or_uid not in self.clients_by_uid:
+                self.clients_by_uid[id_or_uid] = Client(id_or_uid)
+        return self
+
+    def __getitem__(self, id_or_uid):
+        if id_or_uid in self.ident_map:
+            id_or_uid = self.ident_map[id_or_uid]
+        if self.is_id(id_or_uid):
+            if id_or_uid not in self.clients_by_id:
+                self += id_or_uid
+            return self.clients_by_id[id_or_uid]
+        else:
+            if id_or_uid not in self.clients_by_uid:
+                self += id_or_uid
+            return self.clients_by_uid[id_or_uid]
+
+    def __iter__(self):
+        for id_client in self.clients_by_id.values():
+            yield id_client
+        for uid_client in self.clients_by_uid.values():
+            yield uid_client
+
+
+class Client:
+
+    def __init__(self, identifier):
+        # public
+        self.identifier = identifier
+        self.nick = None
+        self.connected = 0
+        self.onlinetime = 0
+        self.kicks = 0
+        self.pkicks = 0
+        self.bans = 0
+        self.pbans = 0
+        self.last_seen = 0
+        # private
+        self._last_connect = 0
+
+    def connect(self, timestamp):
+        '''
+        client connects at "timestamp"
+        '''
+        logging.debug('CONNECT {}'.format(str(self)))
+        self.connected += 1
+        self._last_connect = timestamp
+
+    def disconnect(self, timestamp):
+        '''
+        client disconnects at "timestamp"
+        '''
+        logging.debug('DISCONNECT {}'.format(str(self)))
+        if not self.connected:
+            logging.debug('^ disconnect before connect')
+            raise InvalidLog('disconnect before connect!')
+        self.connected -= 1
+        session_time = timestamp - self._last_connect
+        self.onlinetime += session_time
+        self.last_seen = timestamp
+
+    def kick(self, target):
+        '''
+        client kicks "target" (Client-obj)
+        '''
+        logging.debug('KICK {} -> {}'.format(str(self), str(target)))
+        target.pkicks += 1
+        self.kicks += 1
+
+    def ban(self, target):
+        '''
+        client bans "target" (Client-obj)
+        '''
+        logging.debug('BAN {} -> {}'.format(str(self), str(target)))
+        target.pbans += 1
+        self.bans += 1
+
+    def __str__(self):
+        return '<{},{}>'.format(self.identifier, self.nick)
+
+    def __getitem__(self, item):
+        return {
+            'identifier': self.identifier,
+            'nick': self.nick,
+            'connected': self.connected,
+            'onlinetime': self.onlinetime,
+            'kicks': self.kicks,
+            'pkicks': self.pkicks,
+            'bans': self.bans,
+            'pbans': self.pbans,
+        }[item]
diff --git a/tsstats/config.py b/tsstats/config.py
new file mode 100644
index 0000000..0ebb1f9
--- /dev/null
+++ b/tsstats/config.py
@@ -0,0 +1,22 @@
+from os.path import abspath
+
+from tsstats.exceptions import InvalidConfig
+
+try:
+    from configparser import ConfigParser
+except ImportError:
+    from ConfigParser import ConfigParser
+
+
+def parse_config(config_path):
+    config = ConfigParser()
+    config.read(config_path)
+    if 'General' not in config or not \
+            ('logfile' in config['General'] and
+                'outputfile' in config['General']):
+        raise InvalidConfig
+
+    general = config['General']
+    log_path = abspath(general['logfile'])
+    output_path = abspath(general['outputfile'])
+    return log_path, output_path
diff --git a/tsstats/exceptions.py b/tsstats/exceptions.py
new file mode 100644
index 0000000..371f04d
--- /dev/null
+++ b/tsstats/exceptions.py
@@ -0,0 +1,10 @@
+class InvalidConfig(Exception):
+    pass
+
+
+class InvalidLog(Exception):
+    pass
+
+
+class ConfigNotFound(Exception):
+    pass
diff --git a/tsstats/log.py b/tsstats/log.py
new file mode 100644
index 0000000..8af2575
--- /dev/null
+++ b/tsstats/log.py
@@ -0,0 +1,64 @@
+import logging
+import re
+from datetime import datetime
+from glob import glob
+
+from tsstats.client import Clients
+
+re_dis_connect = re.compile(r"'(.*)'\(id:(\d*)\)")
+re_disconnect_invoker = re.compile(
+    r'invokername=(.*)\ invokeruid=(.*)\ reasonmsg'
+)
+
+
+def parse_logs(log_path, ident_map={}, file_log=False):
+    clients = Clients(ident_map)
+    # setup logging
+    log = logging.getLogger()
+    log.setLevel(logging.DEBUG)
+    if file_log:
+        # file logger
+        file_handler = logging.FileHandler('debug.txt', 'w', 'UTF-8')
+        file_handler.setFormatter(logging.Formatter('%(message)s'))
+        file_handler.setLevel(logging.DEBUG)
+        log.addHandler(file_handler)
+    # stream logger (unused)
+    stream_handler = logging.StreamHandler()
+    stream_handler.setLevel(logging.INFO)
+    log.addHandler(stream_handler)
+
+    # find all log-files and open them TODO: move this into main
+    file_paths = sorted([file_path for file_path in glob(log_path)])
+
+    for file_path in file_paths:
+        log_file = open(file_path)
+        # process lines
+        logging.debug('Started parsing of {}'.format(log_file.name))
+        for line in log_file:
+            parts = line.split('|')
+            log_format = '%Y-%m-%d %H:%M:%S.%f'
+            stripped_time = datetime.strptime(parts[0], log_format)
+            logdatetime = int(stripped_time.timestamp())
+            data = '|'.join(parts[4:]).strip()
+            if data.startswith('client'):
+                nick, clid = re_dis_connect.findall(data)[0]
+                if data.startswith('client connected'):
+                    client = clients[clid]
+                    client.nick = nick
+                    client.connect(logdatetime)
+                elif data.startswith('client disconnected'):
+                    client = clients[clid]
+                    client.nick = nick
+                    client.disconnect(logdatetime)
+                    if 'invokeruid' in data:
+                        re_disconnect_data = re_disconnect_invoker.findall(
+                            data)
+                        invokernick, invokeruid = re_disconnect_data[0]
+                        invoker = clients[invokeruid]
+                        invoker.nick = invokernick
+                        if 'bantime' in data:
+                            invoker.ban(client)
+                        else:
+                            invoker.kick(client)
+        logging.debug('Finished parsing of {}'.format(log_file.name))
+    return clients
diff --git a/template.html b/tsstats/template.html
similarity index 100%
rename from template.html
rename to tsstats/template.html
diff --git a/tsstats/template.py b/tsstats/template.py
new file mode 100644
index 0000000..6520b87
--- /dev/null
+++ b/tsstats/template.py
@@ -0,0 +1,35 @@
+from os.path import abspath
+from time import localtime, strftime
+
+from jinja2 import Environment, FileSystemLoader
+
+from tsstats.utils import seconds_to_text, sort_clients
+
+
+def render_template(clients, output, template_name='tsstats/template.html',
+                    title='TeamspeakStats', debug=False):
+    # prepare clients
+    clients_onlinetime_ = sort_clients(clients.clients_by_id, 'onlinetime')
+    clients_onlinetime = [
+        (client, seconds_to_text(onlinetime))
+        for client, onlinetime in clients_onlinetime_
+    ]
+
+    clients_kicks = sort_clients(clients.clients_by_uid, 'kicks')
+    clients_pkicks = sort_clients(clients.clients_by_id, 'pkicks')
+    clients_bans = sort_clients(clients.clients_by_uid, 'bans')
+    clients_pbans = sort_clients(clients.clients_by_id, 'pbans')
+    objs = [('Onlinetime', clients_onlinetime), ('Kicks', clients_kicks),
+            ('passive Kicks', clients_pkicks),
+            ('Bans', clients_bans), ('passive Bans', clients_pbans)]
+
+    # render
+    template_loader = FileSystemLoader(abspath('.'))
+    template_env = Environment(loader=template_loader)
+
+    def fmttime(timestamp):
+        return strftime('%x %X', localtime(int(timestamp)))
+    template_env.filters['frmttime'] = fmttime
+    template = template_env.get_template(template_name)
+    with open(output, 'w') as f:
+        f.write(template.render(title=title, objs=objs, debug=debug))
diff --git a/tsstats/tests/__init__.py b/tsstats/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tsstats/tests/res/config.ini b/tsstats/tests/res/config.ini
new file mode 100644
index 0000000..6e5f4f6
--- /dev/null
+++ b/tsstats/tests/res/config.ini
@@ -0,0 +1,3 @@
+[General]
+logfile = tsstats/tests/res/test.log
+outputfile = tsstats/tests/res/output.html
diff --git a/tests/res/id_map.json b/tsstats/tests/res/id_map.json
similarity index 100%
rename from tests/res/id_map.json
rename to tsstats/tests/res/id_map.json
diff --git a/tests/res/test.log b/tsstats/tests/res/test.log
similarity index 100%
rename from tests/res/test.log
rename to tsstats/tests/res/test.log
diff --git a/tests/res/test.log.broken b/tsstats/tests/res/test.log.broken
similarity index 100%
rename from tests/res/test.log.broken
rename to tsstats/tests/res/test.log.broken
diff --git a/tests/test_config.py b/tsstats/tests/test_config.py
similarity index 67%
rename from tests/test_config.py
rename to tsstats/tests/test_config.py
index 2eec1a0..823e7d0 100644
--- a/tests/test_config.py
+++ b/tsstats/tests/test_config.py
@@ -1,12 +1,13 @@
 import configparser
 from os import remove
-from os.path import exists
+from os.path import abspath, exists
 
 from nose.tools import raises, with_setup
 
-from tsstats import exceptions, gen_abspath, parse_config
+from tsstats import exceptions
+from tsstats.config import parse_config
 
-configpath = gen_abspath('tests/res/test.cfg')
+configpath = abspath('tsstats/tests/res/test.cfg')
 
 
 def create_config(values, key='General'):
@@ -25,7 +26,7 @@ def clean_config():
 @raises(exceptions.InvalidConfig)
 def test_invalid_config():
     create_config({
-        'loggfile': 'tests/res/test.log',
+        'loggfile': 'tsstats/tests/res/test.log',
         'outputfile': ''
     })
     _, _, _, _ = parse_config(configpath)
@@ -34,10 +35,10 @@ def test_invalid_config():
 @with_setup(clean_config, clean_config)
 def test_config():
     create_config({
-        'logfile': 'tests/res/test.log',
+        'logfile': 'tsstats/tests/res/test.log',
         'outputfile': 'output.html',
         'debug': 'true'
     })
     log_path, output_path = parse_config(configpath)
-    assert log_path == gen_abspath('tests/res/test.log')
-    assert output_path == gen_abspath('output.html')
+    assert log_path == abspath('tsstats/tests/res/test.log')
+    assert output_path == abspath('output.html')
diff --git a/tests/test_general.py b/tsstats/tests/test_general.py
similarity index 73%
rename from tests/test_general.py
rename to tsstats/tests/test_general.py
index 6b76df6..a1afb96 100644
--- a/tests/test_general.py
+++ b/tsstats/tests/test_general.py
@@ -2,13 +2,15 @@ from os import remove
 
 from nose.tools import raises
 
-from tsstats import exceptions, main, parse_logs
+from tsstats import exceptions
+from tsstats.__main__ import main
+from tsstats.log import parse_logs
 
-clients = parse_logs('tests/res/test.log')
+clients = parse_logs('tsstats/tests/res/test.log')
 
 
 def test_main():
-    main(config_path='tests/res/config.ini')
+    main(config_path='tsstats/tests/res/config.ini')
 
 
 @raises(exceptions.ConfigNotFound)
@@ -17,8 +19,8 @@ def test_main_config_not_found():
 
 
 def test_main_idmap_load():
-    main(config_path='tests/res/config.ini',
-         id_map_path='tests/res/id_map.json')
+    main(config_path='tsstats/tests/res/config.ini',
+         id_map_path='tsstats/tests/res/id_map.json')
 
 
 def test_length():
@@ -58,14 +60,14 @@ def test_client_repr():
 
 
 def test_debug_log():
-    clients = parse_logs('tests/res/test.log', file_log=True)
+    clients = parse_logs('tsstats/tests/res/test.log', file_log=True)
     open('debug.txt')
     remove('debug.txt')
 
 
 @raises(exceptions.InvalidLog)
 def test_parse_broken():
-    clients = parse_logs('tests/res/test.log.broken')
+    clients = parse_logs('tsstats/tests/res/test.log.broken')
 
 
 def test_iter_clients():
diff --git a/tests/test_ident_map.py b/tsstats/tests/test_ident_map.py
similarity index 90%
rename from tests/test_ident_map.py
rename to tsstats/tests/test_ident_map.py
index 6b29f52..338fa2f 100644
--- a/tests/test_ident_map.py
+++ b/tsstats/tests/test_ident_map.py
@@ -1,4 +1,4 @@
-from tsstats import Clients
+from tsstats.client import Clients
 
 ident_map = {
     '1': '2',
diff --git a/tests/test_template.py b/tsstats/tests/test_template.py
similarity index 71%
rename from tests/test_template.py
rename to tsstats/tests/test_template.py
index 87d294a..423edf6 100644
--- a/tests/test_template.py
+++ b/tsstats/tests/test_template.py
@@ -2,10 +2,12 @@ from os import remove
 
 from bs4 import BeautifulSoup
 
-from tsstats import _format_seconds, parse_logs, render_template
+from tsstats.utils import seconds_to_text
+from tsstats.log import parse_logs
+from tsstats.template import render_template
 
-output_path = 'tests/res/output.html'
-clients = parse_logs('tests/res/test.log')
+output_path = 'tsstats/tests/res/output.html'
+clients = parse_logs('tsstats/tests/res/test.log')
 
 
 class TestTemplate:
@@ -26,5 +28,5 @@ class TestTemplate:
         render_template(clients, output_path)
         soup = BeautifulSoup(open(output_path), 'html.parser')
         # check onlinetime-data
-        assert _format_seconds(clients['1'].onlinetime) == \
+        assert seconds_to_text(clients['1'].onlinetime) == \
             soup.find('span', class_='badge').text
diff --git a/tsstats/utils.py b/tsstats/utils.py
new file mode 100644
index 0000000..67cd0cb
--- /dev/null
+++ b/tsstats/utils.py
@@ -0,0 +1,13 @@
+def sort_clients(stor, key):
+    clients = stor.values()
+    cl_data = [(client, client[key]) for client in clients if client[key] > 0]
+    return sorted(cl_data, key=lambda data: data[1], reverse=True)
+
+
+def seconds_to_text(seconds):
+    minutes, seconds = divmod(seconds, 60)
+    hours, minutes = divmod(minutes, 60)
+    hours = str(hours) + 'h ' if hours > 0 else ''
+    minutes = str(minutes) + 'm ' if minutes > 0 else ''
+    seconds = str(seconds) + 's' if seconds > 0 else ''
+    return hours + minutes + seconds