Implemented friendly messages after creating and updating mods. Converted line endings to LF.
Implemented friendly messages after creating and updating mods. Converted line endings to LF.

--- a/KerbalStuff/Constants.py
+++ b/KerbalStuff/Constants.py
@@ -1,17 +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)

-

+__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()

--- a/KerbalStuff/Mod.py
+++ b/KerbalStuff/Mod.py
@@ -1,153 +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(

+__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)

--- a/KerbalStuff/ReadOnly.py
+++ b/KerbalStuff/ReadOnly.py
@@ -1,145 +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 = Constants.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

+__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 = Constants.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
+

--- a/KerbalStuff/ReadWrite.py
+++ b/KerbalStuff/ReadWrite.py
@@ -1,138 +1,139 @@
-__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.read(), '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):

-        mod_id = int(mod_id)

-        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.read(), 'application/zip')

-

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

-

-        if cls.current_json is not None:

-            return cls.current_json

-

-        return None

+__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):
+    constants = Constants
+    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.read(), '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):
+        mod_id = int(mod_id)
+        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.read(), 'application/zip')
+
+        cls.do_post_request(Constants.mod_update, mod_id, data=data, files=files, cookies=cls.current_cookies)
+
+        if cls.current_json is not None:
+            return cls.current_json
+
+        return None
+

--- a/KerbalStuff/User.py
+++ b/KerbalStuff/User.py
@@ -1,40 +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,

+__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)

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

-

-from .ReadOnly import KerbalStuffReadOnly

-from .ReadWrite import KerbalStuff

-from .Mod import Mod, ModVersion

+__author__ = 'toadicus'
 
+from .ReadOnly import KerbalStuffReadOnly
+from .ReadWrite import KerbalStuff
+from .Mod import Mod, ModVersion
+

--- a/Namespace/Namespace.py
+++ b/Namespace/Namespace.py
@@ -1,40 +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):

+__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()

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

-

-__all__ = ["Namespace"]

-

-from .Namespace import Namespace

+__author__ = 'toadicus'
 
+__all__ = ["Namespace"]
+
+from .Namespace import Namespace
+

--- a/PyKStuff.py
+++ b/PyKStuff.py
@@ -1,292 +1,309 @@
-__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)

-        print(KerbalStuff.current_json)

-    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__":

+__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)
+        if KerbalStuff.current_json["error"]:
+            sys.stderr.write("Error creating mod '{0}': {1}".format(args.name, KerbalStuff.current_json["reason"]))
+            sys.exit(1)
+        else:
+            sys.stdout.write("Mod created!  New mod id is {0}.  You can view the new mod at {1}{2}".format(
+                KerbalStuff.current_json["id"],
+                KerbalStuff.constants.RootUri,
+                KerbalStuff.current_json["url"]
+            ))
+    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)
+        if KerbalStuff.current_json["error"]:
+            sys.stderr.write("Error updating mod #{0}: {1}".format(args.mod_id, KerbalStuff.current_json["reason"]))
+            sys.exit(1)
+        else:
+            sys.stdout.write("Mod updated!  New version id is {0}.  You can view the new mod version at {1}{2}".format(
+                KerbalStuff.current_json["id"],
+                KerbalStuff.constants.RootUri,
+                KerbalStuff.current_json["url"]
+            ))
+    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())

--- a/StaticClass/StaticClass.py
+++ b/StaticClass/StaticClass.py
@@ -1,22 +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__))

+__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__))
+

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

-

-from .StaticClass import StaticClass

+__author__ = 'toadicus'
+
+from .StaticClass import StaticClass
 from .StaticClass import StaticClass as staticclass