From 61f3e1d7b345a182956d550c239087cc9e00ee5b Mon Sep 17 00:00:00 2001
From: Henke Adolfsson <catears@catears.se>
Date: Mon, 5 Mar 2018 07:42:18 +0100
Subject: [PATCH] Add implementation for #3

---
 .gitignore      |  3 +-
 src/__init__.py |  1 +
 src/bus.py      | 91 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/core.py     | 50 +++++++++++++++++++++++++++
 src/doc.py      |  6 ++++
 src/main.py     | 41 ++++++++++++++++++++++
 6 files changed, 191 insertions(+), 1 deletion(-)
 create mode 100644 src/bus.py
 create mode 100644 src/core.py
 create mode 100644 src/doc.py
 create mode 100644 src/main.py

diff --git a/.gitignore b/.gitignore
index 94b65a4..6eb6012 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 venv/*
 __pycache__/*
-dist/*
\ No newline at end of file
+dist/*
+__pycache__/
\ No newline at end of file
diff --git a/src/__init__.py b/src/__init__.py
index e69de29..918e828 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -0,0 +1 @@
+from main import *
diff --git a/src/bus.py b/src/bus.py
new file mode 100644
index 0000000..3ef95aa
--- /dev/null
+++ b/src/bus.py
@@ -0,0 +1,91 @@
+import collections
+import pydash
+
+
+class BusError(Exception):
+    pass
+
+
+class Bus:
+
+    def __init__(self):
+        self.ID = 0
+        self.providers = {}
+        self.listeners = collections.defaultdict(list)
+
+
+    def provide(self, topic, callback):
+        '''Provide an RPC to be used by the bus.'''
+        self.providers[topic] = callback
+
+
+    def call(self, topic, *args, **kwargs):
+        '''Call an RPC that exists on the bus and any listeners.'''
+
+        # Check if RPC exists, throw error on fault.
+        def non_existant_rpc():
+            return topic not in self.providers
+
+        def raise_RPC_error():
+            raise BusError('No RPC Provider: {}'.format(topic))
+
+
+        # Else call listeners and then return with RPC result.
+        def call_listeners():
+            if topic in self.listeners:
+                for _, listener in self.listeners[topic]:
+                    listener(*args, **kwargs)
+
+        def call_rpc():
+            return self.providers[topic](*args, **kwargs)
+
+        call_listeners_and_rpc = pydash.flow(call_listeners, call_rpc)
+
+        return pydash.cond([
+            (non_existant_rpc, raise_RPC_error),
+            (pydash.stub_true, call_listeners_and_rpc)
+        ])()
+
+
+    def listen(self, topic, callback):
+        '''Listen to a specific topic and receive a callback for when it is called.'''
+        ID = self.ID
+        self.ID += 1
+        self.listeners[topic].append((ID, callback))
+        return ID
+
+
+    def unlisten(self, topic, ID):
+        '''Stop recieving updates for a topic.'''
+
+        def has_id():
+            return any(i == ID for i, _ in self.listeners[topic])
+
+        def remove_id():
+            items = self.listeners[topic]
+            keep = lambda i, _: i != ID
+            self.listeners[topic] = [(a, b) for a, b in items if keep(a, b)]
+
+        return pydash.cond([
+            (has_id, remove_id)
+        ])()
+
+
+    def unprovide(self, topic):
+        '''Unregister a topic.'''
+
+        def has_rpc():
+            return topic in self.providers
+
+        def raise_RPC_error():
+            raise BusError('No RPC Provider: {}'.format(topic))
+
+
+        def remove_RPC():
+            del self.providers[topic]
+
+
+        return pydash.cond([
+            (has_rpc, raise_RPC_error),
+            (pydash.stub_true, remove_RPC)
+        ])()
diff --git a/src/core.py b/src/core.py
new file mode 100644
index 0000000..5ba35da
--- /dev/null
+++ b/src/core.py
@@ -0,0 +1,50 @@
+import os
+import configparser
+
+
+class CredentialsException(Exception):
+    '''An exception type for missing credentials.'''
+
+
+def TouchStructure():
+    '''Initializes the .kattcmd file in the users home directory.'''
+    # Create ~/.kattcmd and store:
+    # - Path to .kattisrc file
+    # - Empty list of plugins
+
+    # [options]
+    # kattisrc=~/.kattisrc (check that it exists)
+    # plugins=[]
+
+    user_folder = os.path.expanduser('~')
+    kattcmd_file = os.path.join(user_folder, '.kattcmd')
+    if not os.path.isfile(kattcmd_file):
+        config = configparser.ConfigParser()
+        config['options'] = {
+            'kattisrc': os.path.expanduser('~/.kattisrc'),
+            'plugins': []
+        }
+
+        with open(kattcmd_file, 'w') as configfile:
+            configfile.write(config)
+
+        return True
+    return False
+
+
+def _ListBuiltins():
+    '''Returns a list of all the builtin plugins.'''
+    return []
+
+
+def _ListExternals():
+    '''Returns a list of all user-added plugins.'''
+    config_path = os.path.expanduser('~/.kattcmd')
+    config = configparser.ConfigParser()
+    config.read(config_path)
+    return config['options']['plugins']
+
+
+def ListPlugins():
+    '''Returns a list of all plugins, which is all available commands used by the program.'''
+    return _ListBuiltins() + _ListExternals()
diff --git a/src/doc.py b/src/doc.py
new file mode 100644
index 0000000..854eb38
--- /dev/null
+++ b/src/doc.py
@@ -0,0 +1,6 @@
+
+Interactive = '''If the session should be interactive or not. Brings up the prompt.'''
+
+Command = '''What command to run, cannot be used with --interactive'''
+
+
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..64d49af
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,41 @@
+import os
+import pydash
+import click
+
+import doc
+import bus
+import core
+
+
+def interactive_mode(bus, plugins):
+    pass
+
+
+def command_mode(bus, plugins, command):
+    pass
+
+
+@click.command()
+@click.option('--interactive', default=True, type=bool, help=doc.Interactive)
+@click.option('--command', default='', help=doc.Command)
+def main(interactive, command):
+    '''Command line tool for helping with administrative tasks around Kattis.'''
+    the_bus = bus.Bus()
+
+    core.TouchStructure()
+    plugins = core.ListPlugins()
+    for plugin in plugins:
+        plugin.Init(the_bus)
+
+
+    # Check if interactive or not, if yes: run as interactive, if not then run command
+    if interactive:
+        interactive_mode(the_bus, plugins)
+    elif not command:
+        raise ValueError('Either you must specify a command or use interactive mode.')
+    else:
+        command_mode(the_bus, plugins, command)
+
+
+if __name__ == '__main__':
+    main()
-- 
GitLab