Initial commit.
Initial commit.

--- /dev/null
+++ b/KerbalStuff/Constants.py
@@ -1,1 +1,17 @@
-
+__author__ = 'toadicus'

+

+__all__ = ["Constants"]

+

+from Namespace import Namespace

+

+

+class _Constants(Namespace):

+    def __init__(self):

+        if self._initialized:

+            return

+        super().__init__()

+

+    def format_action(self, uri_path_fmt):

+        return "{0}{1}".format(self.ApiUri, uri_path_fmt)

+

+Constants = _Constants()

--- /dev/null
+++ b/KerbalStuff/Mod.py
@@ -1,1 +1,153 @@
-
+__author__ = 'toadicus'

+

+

+class Mod:

+    def __init__(self, *args, **kwargs):

+        self.versions = []

+        """:type : list[ModVersion]"""

+        self.author = None

+        """:type : str"""

+        self.downloads = None

+        """:type : int"""

+        self.default_version_id = None

+        """:type : int"""

+        self.followers = None

+        """:type : int"""

+        self.id = None

+        """:type : int"""

+        self.ksp_version = None

+        """:type : str"""

+        self.name = None

+        """:type : str"""

+        self.short_description = None

+        """:type : str"""

+        self.license = None

+        """:type : str"""

+

+        error_message = None

+        try:

+            self._init_from_args(*args, **kwargs)

+        except TypeError as x1:

+            try:

+                self._init_from_dict(*args, **kwargs)

+            except TypeError as x2:

+                error_message = "Could not build mod object:\n\t{0}\n\t{1}".format(x1.args[0], x2.args[0])

+

+        if error_message is not None:

+            raise TypeError(error_message)

+

+    def _init_from_dict(self, json_dict: dict):

+        """

+        Initalizes a new instance of the Mod class from a dict of json objects.

+        :param json_dict: dict containing the deserialized json response from KerbalStuff

+        """

+        self.author = str(json_dict["author"])

+        self.downloads = int(json_dict["downloads"])

+        self.default_version_id = int(json_dict["default_version_id"])

+        self.followers = int(json_dict["followers"])

+        self.id = int(json_dict["id"])

+        self.name = str(json_dict["name"])

+        self.short_description = str(json_dict["short_description"])

+

+        if "background" in json_dict.keys():

+            self.background = json_dict["background"]

+        else:

+            self.background = None

+

+        if self.background is not None and "bg_offset_y" in json_dict.keys():

+                self.bg_offset_y = json_dict["bg_offset_y"]

+

+        if "versions" in json_dict.keys() and isinstance(json_dict["versions"], list):

+            for v in json_dict["versions"]:

+                self.versions.append(ModVersion(v))

+

+    def _init_from_args(self,

+                        name: str,

+                        short_description: str,

+                        friendly_version: str,

+                        ksp_version: str,

+                        mod_license: str

+                        ):

+        """

+        Initializes a Mod object from the given arguments.  Used in mod creation.

+        :param name: The name of this Mod

+        :param short_description: A short (1000 characters or less) description of this Mod

+        :param friendly_version: The human-friendly (or not) version name or number used in creating this Mod

+        :param ksp_version: The primary version of KSP for which this Mod was developed.

+        :param mod_license: The name or title (128 characters or less) of thie License under which this Mod is released.

+        """

+        self.name = name

+        self.short_description = short_description

+        self.license = mod_license

+

+        self.versions.append(ModVersion(friendly_version, ksp_version))

+

+    def __str__(self):

+        return ("Mod: {1}\n" +

+                "id: {6}\n" +

+                "author: {3}\n" +

+                "downloads: {0}\n" +

+                "followers: {2}\n" +

+                "short_description: {7}\n" +

+                "default_version_id: {4}\n" +

+                "versions:\n[\n{5}\n]").format(

+                    self.downloads,

+                    self.name,

+                    self.followers,

+                    self.author,

+                    self.default_version_id,

+                    "\n".join([str(v) for v in self.versions]),

+                    self.id,

+                    self.short_description)

+

+

+class ModVersion:

+    def __init__(self, *args, **kwargs):

+        self.changelog = None

+        """:type : string"""

+        self.download_path = None

+        """:type : string"""

+        self.friendly_version = None

+        """:type : string"""

+        self.id = None

+        """:type : int"""

+        self.ksp_version = None

+        """:type : string"""

+

+        error_message = None

+        try:

+            self._init_from_args(*args, **kwargs)

+        except TypeError as x1:

+            try:

+                self._init_from_dict(*args, **kwargs)

+            except TypeError as x2:

+                error_message = "Could not build mod object:\n\t{0}\n\t{1}".format(x1.args[0], x2.args[0])

+

+        if error_message is not None:

+            raise TypeError(error_message)

+

+    def _init_from_dict(self, json_dict: dict):

+        """

+        Initalizes a new instance of the ModVersion class from a dict of json objects.

+        :param json_dict: dict containing the deserialized json response from KerbalStuff

+        """

+        self.changelog = json_dict["changelog"]

+        self.download_path = json_dict["download_path"]

+        self.id = json_dict["id"]

+        self.ksp_version = json_dict["ksp_version"]

+        self.friendly_version = json_dict["friendly_version"]

+

+    def _init_from_args(self, friendly_version: str, ksp_version: str, changelog: str=None):

+        """

+

+        :param friendly_version: The human-friendly (or not) version name or number used in creating this ModVersion

+        :param ksp_version: The primary version of KSP for which this ModVersion was developed.

+        :param changelog: An optional log describing the changes made in this ModVersion

+        """

+        self.friendly_version = friendly_version

+        self.ksp_version = ksp_version

+        self.changelog = changelog

+

+    def __str__(self):

+        return "Version {0}\n\tid: {1}\n\tksp_version: {2}\n\tdownload_path: {3}\n\tchangelog: {4}".format(

+            self.friendly_version, self.id, self.ksp_version, self.download_path, self.changelog)

--- /dev/null
+++ b/KerbalStuff/ReadOnly.py
@@ -1,1 +1,145 @@
+__author__ = 'toadicus'

+

+import requests

+import sys

+from .Constants import Constants

+from .Mod import Mod, ModVersion

+from .User import User

+from StaticClass import staticclass

+

+Constants.RootUri = "https://kerbalstuff.com"

+Constants.ApiUri = self.RootUri + "/api"

+Constants.UserAgent = "PyKStuff by toadicus"

+

+Constants.browse_new = Constants.format_action("/browse/new?page={0:d}")

+Constants.browse_featured = Constants.format_action("/browse/featured?page={0:d}")

+Constants.browse_top = Constants.format_action("/browse/top?page={0:d}")

+

+Constants.mod_info = Constants.format_action("/mod/{0:d}")

+Constants.mod_latest = Constants.format_action("/mod/{0:d}/latest")

+

+Constants.search_mod = Constants.format_action("/search/mod?query={0}&page={1:d}")

+Constants.search_user = Constants.format_action("/search/user?query={0}&page={1:d}")

+

+Constants.user_info = Constants.format_action("/user/{0}")

+

+if Constants.headers is None:

+    Constants.headers = {}

+Constants.headers["User-Agent"] = "PyKStuff by toadicus"

+

+

+@staticclass

+class KerbalStuffReadOnly:

+    current_response = None

+    """:type : requests.Response"""

+    current_json = None

+    """:type : dict[str, object]"""

+

+    @classmethod

+    def do_get_request(cls, uri: str, *format_args):

+        cls.current_response = None

+        cls.current_json = None

+

+        if len(format_args) > 0:

+            uri = uri.format(*format_args)

+

+        cls.current_response = requests.get(uri, headers=Constants.headers)

+

+        if cls.current_response.status_code >= 400:

+            sys.stderr.write("Failed connecting to KerbalStuff with status code {0}: {1}".format(

+                cls.current_response.status_code, cls.current_response.reason))

+            sys.exit(cls.current_response.status_code)

+

+        try:

+            cls.current_json = cls.current_response.json()

+        except ValueError:

+            sys.stderr.write("Response received was not valid JSON\n")

+            sys.exit(1)

+

+    @classmethod

+    def browse_featured(cls, page_id: int=1) -> list:

+        cls.do_get_request(Constants.browse_featured, page_id)

+

+        mods = []

+        """:type : list of [Mod]"""

+

+        if cls.current_json is not None and isinstance(cls.current_json, list):

+            for m in cls.current_json:

+                mods.append(Mod(m))

+

+        return mods

+

+    @classmethod

+    def browse_new(cls, page_id: int=1):

+        cls.do_get_request(Constants.browse_new, page_id)

+

+        mods = []

+        """:type : list of [Mod]"""

+

+        if cls.current_json is not None and isinstance(cls.current_json, list):

+            for m in cls.current_json:

+                mods.append(Mod(m))

+

+        return mods

+

+    @classmethod

+    def browse_top(cls, page_id: int=1):

+        cls.do_get_request(Constants.browse_top, page_id)

+

+        mods = []

+        """:type : list of [Mod]"""

+

+        if cls.current_json is not None and isinstance(cls.current_json, list):

+            for m in cls.current_json:

+                mods.append(Mod(m))

+

+        return mods

+

+    @classmethod

+    def mod_info(cls, mod_id: int) -> Mod:

+        cls.do_get_request(Constants.mod_info, mod_id)

+

+        if cls.current_json is not None:

+            return Mod(cls.current_json)

+        return None

+

+    @classmethod

+    def mod_latest(cls, mod_id: int) -> ModVersion:

+        cls.do_get_request(Constants.mod_latest, mod_id)

+

+        if cls.current_json is not None:

+            return ModVersion(cls.current_json)

+        return None

+

+    @classmethod

+    def search_mod(cls, query: str, page_id: int=1):

+        cls.do_get_request(Constants.search_mod, query, page_id)

+

+        mods = []

+        """:type : list of [Mod]"""

+

+        if cls.current_json is not None and isinstance(cls.current_json, list):

+            for m in cls.current_json:

+                mods.append(Mod(m))

+

+        return mods

+

+    @classmethod

+    def search_user(cls, query: str, page_id: int=0):

+        cls.do_get_request(Constants.search_user, query, page_id)

+

+        users = []

+

+        if cls.current_json is not None and isinstance(cls.current_json, list):

+            for u in cls.current_json:

+                users.append(User(u))

+        return users

+

+    @classmethod

+    def user_info(cls, username: str):

+        cls.do_get_request(Constants.user_info, username)

+

+        if cls.current_json is not None and isinstance(cls.current_json, dict):

+            return User(cls.current_json)

+        return None

 

--- /dev/null
+++ b/KerbalStuff/ReadWrite.py
@@ -1,1 +1,137 @@
+__author__ = 'toadicus'

+

+import os

+import requests

+import zipfile

+

+from requests.cookies import RequestsCookieJar

+from .ReadOnly import KerbalStuffReadOnly

+from .Constants import Constants

+from .Mod import Mod, ModVersion

+

+Constants.login = Constants.format_action("/login")

+Constants.mod_create = Constants.format_action("/mod/create")

+Constants.mod_update = Constants.format_action("/mod/{0:d}/update")

+

+

+class KerbalStuff(KerbalStuffReadOnly):

+    current_cookies = None

+    """:type : RequestsCookieJar"""

+    @classmethod

+    def do_post_request(cls,

+                        uri: str,

+                        *format_args,

+                        data: dict=None,

+                        files: dict=None,

+                        cookies: RequestsCookieJar=None

+                        ):

+        cls.current_response = None

+        cls.current_json = None

+        cls.current_cookies = None

+

+        if len(format_args) > 0:

+            uri = uri.format(*format_args)

+

+        cls.current_response = requests.post(uri, headers=Constants.headers, data=data, files=files, cookies=cookies)

+

+        try:

+            cls.current_json = cls.current_response.json()

+        except ValueError:

+            sys.stderr.write("Response received was not valid JSON\n")

+            sys.exit(1)

+

+    @classmethod

+    def login(cls, username: str, password: str):

+        data = {'username': username, 'password': password}

+

+        cls.do_post_request(Constants.login, data=data)

+

+        if cls.current_json is not None and not cls.current_json["error"]:

+            cls.current_cookies = cls.current_response.cookies

+

+    @classmethod

+    def mod_create(cls, mod: Mod, file_name: str, file_path: str):

+        if not Mod or not isinstance(mod, Mod):

+            raise TypeError("mod argument must be a valid Mod object")

+

+        if mod.name is None or len(mod.name) == 0:

+            raise TypeError("mod.name cannot be empty")

+        if mod.license is None or len(mod.license) == 0:

+            raise TypeError("mod.license cannot be empty")

+        if mod.short_description is None or len(mod.short_description) == 0:

+            raise TypeError("mod.short_description cannot be empty")

+        if len(mod.versions) != 1:

+            raise TypeError("mod must have a single version to create")

+        if mod.versions[0] is None:

+            raise TypeError("First mod version must be a valid ModVersion object")

+        if mod.versions[0].friendly_version is None or len(mod.versions[0].friendly_version) == 0:

+            raise TypeError("First mod version friendly_version cannot be empty")

+        if mod.versions[0].ksp_version is None or len(mod.versions[0].ksp_version) == 0:

+            raise TypeError("First mod version ksp_version cannot be empty")

+

+        if cls.current_cookies is None or "session" not in cls.current_cookies.keys():

+            raise Exception("Must log in first!")

+

+        if not os.path.exists(file_path):

+            raise IOError("File '{0}' does not exist".format(file_path))

+

+        data = {

+            'name': mod.name,

+            'short-description': mod.short_description,

+            'license': mod.license,

+            'version': mod.versions[0].friendly_version,

+            'ksp-version': mod.versions[0].ksp_version

+        }

+

+        files = {}

+

+        with open(file_path, 'rb') as zip_file:

+            if not zipfile.is_zipfile(zip_file):

+                raise IOError("File at path '{0}' is not a valid zip file.".format(file_path))

+            files['zipball'] = (file_name, zip_file.readall(), 'application/zip')

+

+        cls.do_post_request(Constants.mod_create, data=data, files=files, cookies=cls.current_cookies)

+

+        if cls.current_json is not None:

+            return cls.current_json

+

+        return None

+

+    @classmethod

+    def mod_update(cls, mod_id: int, mod_version: ModVersion, notify_followers: bool, file_name: str, file_path: str):

+        if mod_version is None or not isinstance(mod_version, ModVersion):

+            raise TypeError("mod_version must be a valid ModVersion object")

+        if mod_version.friendly_version is None or len(mod_version.friendly_version) == 0:

+            raise TypeError("mod_version.friendly_version must not be empty")

+        if mod_version.ksp_version is None or len(mod_version.ksp_version) == 0:

+            raise TypeError("mod_version.ksp_version must not be empty")

+

+        if cls.current_cookies is None or "session" not in cls.current_cookies.keys():

+            raise Exception("Must log in first!")

+

+        if not os.path.exists(file_path):

+            raise IOError("File '{0}' does not exist".format(file_path))

+

+        data = {

+            'version': mod_version.friendly_version,

+            'ksp-version': mod_version.ksp_version,

+            'notify-followers': "yes" if notify_followers else "no"

+        }

+

+        if mod_version.changelog is not None and len(mod_version.changelog) > 0:

+            data['changelog'] = mod_version.changelog

+

+        files = {}

+

+        with open(file_path, 'rb') as zip_file:

+            if not zipfile.is_zipfile(zip_file):

+                raise IOError("File at path '{0}' is not a valid zip file.".format(file_path))

+            files['zipball'] = (file_name, zip_file.readall(), 'application/zip')

+

+        cls.do_post_request(Constants.mod_update, mod_id, data=data, files=file, cookies=cls.current_cookies)

+

+        if cls.current_json is not None:

+            return cls.current_json

+

+        return None

 

--- /dev/null
+++ b/KerbalStuff/User.py
@@ -1,1 +1,40 @@
-
+__author__ = 'toadicus'

+

+from .Mod import Mod

+

+

+class User:

+    def __init__(self, json_dict: dict):

+        self.description = json_dict["description"]

+        """:type : str"""

+        self.forum_username = json_dict["forumUsername"]

+        """:type : str"""

+        self.irc_nick = json_dict["ircNick"]

+        """:type : str"""

+        self.mods = []

+        """:type : list[Mod]"""

+        for m in json_dict["mods"]:

+            self.mods.append(Mod(m))

+

+        self.username = json_dict["username"]

+        """:type : str"""

+        self.reddit_username = json_dict["redditUsername"]

+        """:type : str"""

+        self.twitter_username = json_dict["twitterUsername"]

+        """:type : str"""

+

+    def __str__(self):

+        return "User: username={0}," \

+               "twitterUsername={1}," \

+               "redditUsername={3}," \

+               "ircNick={4}," \

+               "description={5}," \

+               "forumUsername={6}" \

+               "\nmods:\n{2}".format(

+                                self.username,

+                                self.twitter_username,

+                                "\n".join([str(m) for m in self.mods]),

+                                self.reddit_username,

+                                self.irc_nick,

+                                self.description,

+                                self.forum_username)

--- /dev/null
+++ b/KerbalStuff/__init__.py
@@ -1,1 +1,6 @@
+__author__ = 'toadicus'

+

+from .ReadOnly import KerbalStuffReadOnly

+from .ReadWrite import KerbalStuff

+from .Mod import Mod, ModVersion

 

--- /dev/null
+++ b/Namespace/Namespace.py
@@ -1,1 +1,40 @@
-
+__author__ = 'toadicus'

+

+

+class Namespace():

+    _instance = None

+

+    @classmethod

+    def __new__(cls, *args, **kwargs):

+        if cls._instance is None:

+            obj = super(Namespace, cls).__new__(*args, **kwargs)

+            obj._config = {}

+            obj._allow_reassignment = True

+            obj._initialized = False

+            cls._instance = obj

+        return cls._instance

+

+    def __init__(self, allow_reassignment=True):

+        if self._initialized:

+            return

+        self._initialized = True

+        self._allow_reassignment = allow_reassignment

+

+    def __getattr__(self, key):

+        if isinstance(key, str) and key[0] == "_":

+            return object.__getattribute__(self, key)

+        try:

+            return self._config[key]

+        except KeyError:

+            return None

+

+    def __setattr__(self, key, value):

+        if isinstance(key, str) and key[0] == "_":

+            object.__setattr__(self, key, value)

+            return

+        if not self._allow_reassignment and key in self._config.keys():

+            raise TypeError("Attempted to reassign Namespace member {0}, but reassignment is not allowed.".format(key))

+        self._config[key] = value

+

+    def __contains__(self, key):

+        return key in self._config.keys()

--- /dev/null
+++ b/Namespace/__init__.py
@@ -1,1 +1,6 @@
+__author__ = 'toadicus'

+

+__all__ = ["Namespace"]

+

+from .Namespace import Namespace

 

file:b/PyKStuff.py (new)
--- /dev/null
+++ b/PyKStuff.py
@@ -1,1 +1,291 @@
-
+__author__ = 'toadicus'

+__all__ = []

+

+import argparse

+import os

+import sys

+

+from KerbalStuff import KerbalStuff, Mod, ModVersion

+from zipfile import is_zipfile

+

+parser = argparse.ArgumentParser(description="Interact with the KerbalStuff API.")

+actions = parser.add_subparsers(title="actions")

+""":type : argparse._SubParsersAction"""

+

+

+def parser_command(command_name: str, arguments: dict=[], **kwargs: dict) -> argparse.ArgumentParser:

+    def meta_decorator(func) -> argparse.ArgumentParser:

+        action = actions.add_parser(command_name, **kwargs)

+        """:type : argparse.ArgumentParser"""

+        for tuple in arguments:

+            action.add_argument(*tuple[0], **tuple[1])

+        action.set_defaults(func=func)

+        return action

+    return meta_decorator

+

+

+@parser_command(

+    'bf',

+    arguments=[

+        (

+            ['page_id'],

+            {'type': int, 'nargs': '?', 'help': 'optional page number for browse less-new mods', 'default': 1}

+        )

+    ],

+    help="Fetches a list of featured mods from KerbalStuff"

+)

+def browse_featured(args):

+    for m in KerbalStuff.browse_featured(args.page_id):

+        print(m)

+

+

+@parser_command(

+    'bn',

+    arguments=[

+        (

+            ['page_id'],

+            {'type': int, 'nargs': '?', 'help': 'optional page number for browse less-new mods', 'default': 1}

+        )

+    ],

+    help="Fetches a list of new mods from KerbalStuff"

+)

+def browse_new(args):

+    for m in KerbalStuff.browse_new(args.page_id):

+        print(m)

+

+

+@parser_command(

+    'bt',

+    arguments=[

+        (

+            ['page_id'],

+            {'type': int, 'nargs': '?', 'help': 'optional page number for browse less-new mods', 'default': 1}

+        )

+    ],

+    help="Fetches a list of top mods from KerbalStuff"

+)

+def browse_top(args):

+    for m in KerbalStuff.browse_top(args.page_id):

+        print(m)

+

+

+@parser_command(

+    'mi',

+    arguments=[

+        (

+            ['mod_id'],

+            {'type': int}

+        )

+    ],

+    help="Fetches info about a single mod from KerbalStuff"

+)

+def mod_info(args):

+    print(KerbalStuff.mod_info(args.mod_id))

+

+

+@parser_command(

+    'ml',

+    arguments=[

+        (

+            ['mod_id'],

+            {'type': int}

+        )

+    ],

+    help="Fetches info about the latest version of a single mod from KerbalStuff"

+)

+def mod_latest(args):

+    print(KerbalStuff.mod_latest(args.mod_id))

+

+

+@parser_command(

+    'sm',

+    arguments=[

+        (

+            ['query'],

+            {'type': str}

+        )

+    ],

+    help="Searches KerbalStuff for mods matching the given query and prints a list of any results."

+)

+def search_mod(args):

+    for m in KerbalStuff.search_mod(args.query):

+        print(m)

+

+

+@parser_command(

+    'su',

+    arguments=[

+        (

+            ['query'],

+            {'type': str}

+        )

+    ],

+    help="Searches KerbalStuff for users matching the given query and prints a list of any results."

+)

+def search_user(args):

+    for u in KerbalStuff.search_user(args.query):

+        print(u)

+

+

+@parser_command(

+    'ui',

+    arguments=[

+        (

+            ['username'],

+            {'type': str, 'help': 'the username of the desired user'}

+        )

+    ],

+    help="Fetches info about a single user from KerbalStuff"

+)

+def user_info(args):

+    print(KerbalStuff.user_info(args.username))

+

+login_arguments = [

+    (

+        ['--username', '-u'],

+        {'type': str, 'help': 'the username to use in logging on', 'required': True}

+    ),

+    (

+        ['--password', '-p'],

+        {'type': str, 'help': 'the password to use in logging on', 'required': True}

+    )

+]

+

+

+@parser_command(

+    'mc',

+    login_arguments + [

+        (

+            ['--name', '-n'],

+            {'type': str, 'help': 'the name of the new mod', 'required': True}

+        ),

+        (

+            ['--desc', '-d'],

+            {'type': str, 'help': 'a short description (1000 characters or less) description of the new mod', 'required': True}

+        ),

+        (

+            ['--version', '-v'],

+            {'type': str, 'help': 'the human-friendly version name or number for the first version of the new mod', 'required': True}

+        ),

+        (

+            ['--ksp', '-k'],

+            {'type': str, 'help': 'the primary version of KSP with which the new mod is compatible', 'required': True}

+        ),

+        (

+            ['--license', '-l'],

+            {

+                'type': str,

+                'help': 'the name or title (128 characters or less) of the license under which the new mod is released',

+                'required': True

+            }

+        ),

+        (

+            ['file'],

+            {'type': str, 'help': 'the zip file to upload when creating the mod'}

+        )

+    ],

+    help='Upload a new mod to KerbalStuff'

+)

+def mod_create(args):

+    KerbalStuff.login(args.username, args.password)

+

+    if KerbalStuff.current_json is None:

+        sys.stderr.write("Login response was not valid JSON; aborting.")

+        sys.exit(1)

+

+    if KerbalStuff.current_json['error']:

+        sys.stderr.write("Error during login: {0}".format(KerbalStuff.current_json['reason']))

+        sys.exit(1)

+

+    file_path = args.file

+

+    if os.path.isfile(file_path):

+        if not is_zipfile(file_path):

+            sys.stderr.write("Not a valid zip file: '{0}'\n".format(file_path))

+            sys.exit(1)

+        file_name = os.path.basename(file_path)

+    else:

+        sys.stderr.write("File does not exist: '{0}'\n".format(file_path))

+        sys.exit(1)

+

+    mod = Mod(args.name, args.desc, args.version, args.ksp, args.license)

+

+    try:

+        KerbalStuff.mod_create(mod, file_name, file_path)

+    except TypeError as x:

+        sys.stderr.write(x)

+        sys.exit(1)

+

+

+@parser_command(

+    'mu',

+    login_arguments + [

+        (

+            ['--mod_id', '-m'],

+            {'type': str, 'help': 'the id of the mod to be updated', 'required': True}

+        ),

+        (

+            ['--notify', '-n'],

+            {'action': 'store_true', 'help': "if used, the mod's followers will be notified of this update"}

+        ),

+        (

+            ['--changelog', '-c'],

+            {'type': str, 'help': 'an optional log describing the changes in this update'}

+        ),

+        (

+            ['--version', '-v'],

+            {'type': str, 'help': 'the human-friendly version name or number for the first version of the new mod', 'required': True}

+        ),

+        (

+            ['--ksp', '-k'],

+            {'type': str, 'help': 'the primary version of KSP with which the new mod is compatible', 'required': True}

+        ),

+        (

+            ['file'],

+            {'type': str, 'help': 'the zip file to upload when creating the mod'}

+        )

+    ],

+    help='Update an existing mod on KerbalStuff'

+)

+def mod_update(args):

+    KerbalStuff.login(args.username, args.password)

+

+    if KerbalStuff.current_json is None:

+        sys.stderr.write("Login response was not valid JSON; aborting.")

+        sys.exit(1)

+

+    if KerbalStuff.current_json['error']:

+        sys.stderr.write("Error during login: {0}".format(KerbalStuff.current_json['reason']))

+        sys.exit(1)

+

+    file_path = args.file

+

+    if os.path.isfile(file_path):

+        if not is_zipfile(file_path):

+            sys.stderr.write("Not a valid zip file: '{0}'\n".format(file_path))

+            sys.exit(1)

+        file_name = os.path.basename(file_path)

+    else:

+        sys.stderr.write("File does not exist: '{0}'\n".format(file_path))

+        sys.exit(1)

+

+    if 'changelog' in args and args.changelog is not None and len(args.changelog) > 0:

+        ver = ModVersion(args.version, args.ksp, args.changelog)

+    else:

+        ver = ModVersion(args.version, args.ksp)

+

+    try:

+        KerbalStuff.mod_update(args.mod_id, ver, args.notify, file_name, file_path)

+    except TypeError as x:

+        sys.stderr.write(x)

+        sys.exit(1)

+

+

+def main():

+    if len(sys.argv) == 1:

+        sys.argv.append('-h')

+    args = parser.parse_args()

+    args.func(args)

+

+if __name__ == "__main__":

+    sys.exit(main())

--- /dev/null
+++ b/StaticClass/StaticClass.py
@@ -1,1 +1,22 @@
+__author__ = 'toadicus'

+

+from types import FunctionType

+

+

+class StaticClass(type):

+    def __new__(mcs, cls, b=None, d=None):

+        if b is not None and d is not None:

+            cls = type(cls, b, d)

+        cls.__new__ = classmethod(StaticClass.raise_on_new)

+

+        for k, v in cls.__dict__.items():

+            if isinstance(v, FunctionType):

+                raise TypeError("Error creating static class '{0}': "

+                                "member function '{1}' is not a class or static method".format(cls.__name__, k))

+

+        return cls

+

+    @staticmethod

+    def raise_on_new(cls, *args, **kwargs):

+        raise TypeError("Cannot create new object of type '{0}': '{0}' is a static class".format(cls.__name__))

 

--- /dev/null
+++ b/StaticClass/__init__.py
@@ -1,1 +1,4 @@
-
+__author__ = 'toadicus'

+

+from .StaticClass import StaticClass

+from .StaticClass import StaticClass as staticclass

file:b/ks (new)
--- /dev/null
+++ b/ks
@@ -1,1 +1,3 @@
-
+#!/bin/bash

+

+python3 PyKStuff.py $*