diff --git a/.gitignore b/.gitignore index c79e78b..22bc45b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Compiled Python *.pyc +# Version Checker Text File +latest.txt + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/internal/extra.py b/internal/extra.py index cf6b078..9e981f4 100644 --- a/internal/extra.py +++ b/internal/extra.py @@ -10,7 +10,7 @@ class notes: name = ("PyOS") - ver = ("1.2.1") + ver = ("2.0") author = ("Prouser123") helpmsg = ("Type 'help' for a list of commands.") @@ -42,4 +42,3 @@ def isFloat(var): def cls(): os.system('cls' if os.name=='nt' else 'clear') - \ No newline at end of file diff --git a/internal/runCommand.py b/internal/runCommand.py index cdfd689..4ed0449 100644 --- a/internal/runCommand.py +++ b/internal/runCommand.py @@ -7,8 +7,10 @@ import sys # PyOS Scripts import internal.extra -import internal.calculator -import internal.sysinfo +import internal.update.runscript +# External Programs +import programs.calculator +import programs.sysinfo # Commands @@ -26,24 +28,34 @@ def help(): @staticmethod def clear(): + # Run built-in function internal.extra.cls() @staticmethod def cls(): + # Run built-in function return commands.clear() @staticmethod def about(): + # Run code below (no function necessary) print(internal.extra.notes.name + " " + internal.extra.notes.ver) print("Author: " + internal.extra.notes.author) @staticmethod def calc(): - internal.calculator.start() + # Run external file + programs.calculator.app() @staticmethod def sysinfo(): - internal.sysinfo.start() + # Run external file + programs.sysinfo.app() + + @staticmethod + def updater(): + # Run updater file + internal.update.runscript.app() # Check if command is valid def isValid(command): @@ -56,4 +68,3 @@ def isValid(command): else: # Print an error print (internal.extra.colors.FAIL + "Invalid command. " + internal.extra.notes.helpmsg + internal.extra.colors.ENDC) - \ No newline at end of file diff --git a/internal/update/__init__.py b/internal/update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/internal/update/ghstatscustom.py b/internal/update/ghstatscustom.py new file mode 100644 index 0000000..d17860a --- /dev/null +++ b/internal/update/ghstatscustom.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python + +""" +Python script to obtain GitHub Release download count and other statistics. + +Based on GitHub Download Stats, modified for use. +Original Author: Alexander Gorishnyak +Link: https://github.com/kefir500/ghstats +""" + +import os +import sys +import json +import time + +try: + import urllib2 +except ImportError: + import urllib.request as urllib2 + +try: + input = raw_input +except NameError: + pass + +__version__ = "1.2.0" +__author__ = "Alexander Gorishnyak" +__email__ = "kefir500@gmail.com" +__license__ = "MIT" + + +class ConnectionError(Exception): + """ + Raised on connection error. + """ + + def __init__(self, reason=None): + """ + :param reason: Connection error reason. + """ + self.reason = reason if reason else "Unknown reason." + + def __str__(self): + return str(self.reason) + + +class GithubError(Exception): + """ + Generic exception raised on GitHub API HTTP error. + """ + + def __init__(self, code, message=None): + """ + :param code: HTTP error code. + :param message: Exception message text. + """ + self.code = code + self.message = message + + def __str__(self): + return "GitHub API HTTP {0}{1}".format(self.code, (": " + self.message) if self.message else "") + + +class GithubRepoError(GithubError): + """ + Raised when accessing nonexistent GitHub username, repository or release tag. + """ + + def __init__(self, message=None): + """ + :param message: Exception message text. + """ + self.code = 404 + self.message = message if message else "Invalid GitHub username, repository or release tag." + + +class GithubLimitError(GithubError): + """ + Raised when GitHub API request limit is exceeded. + """ + + def __init__(self, message=None): + """ + :param message: Exception message text. + """ + self.code = 403 + self.message = message if message else "Request limit exceeded." + + +class GithubTokenError(GithubError): + """ + Raised when trying to pass invalid GitHub OAuth token. + """ + + def __init__(self, message=None): + """ + :param message: Exception message text. + """ + self.code = 401 + self.message = message if message else "Unauthorized. Check your \"GITHUB_TOKEN\" environment variable." + + +class _Text: + """ + Definitions for colored output (no ANSI colors on Windows). + """ + if os.name != "nt": + HEADER = "\033[1;36m" + SUCCESS = "\033[1;32m" + ERROR = "\033[1;31m" + BOLD = "\033[1m" + ITALIC = "\033[3m" + UNDERLINE = "\033[4m" + END = "\033[0m" + else: + HEADER = SUCCESS = ERROR = BOLD = ITALIC = UNDERLINE = END = "" + + +def error(message): + """ + Halt script due to critical error. + :param message: Error text. + """ + sys.exit(_Text.ERROR + "Error: " + message + _Text.END) + + +def get_env_token(): + """ + Get GitHub OAuth token from "GITHUB_TOKEN" environment variable. + :return: GitHub OAuth token. + """ + token = None if "GITHUB_TOKEN" not in os.environ else os.environ["GITHUB_TOKEN"] + return token + + +def print_help(): + """ + Display command line help. + """ + print("GitHub Download Stats") + print("Python script to obtain GitHub Release download count and other statistics.\n") + print("Usage:\n" + " python ghstats.py [{0}user{1}] [{0}repo{1}] [{0}tag{1}] [{0}options{1}]\n" + " python ghstats.py [{0}user{1}/{0}repo{1}] [{0}tag{1}] [{0}options{1}]\n" + .format(_Text.ITALIC, _Text.END)) + print("Arguments:\n" + " {0}user{1} Repository owner. If not present, user will be prompted for input.\n" + " {0}repo{1} Repository title. If not present, user will be prompted for input.\n" + " {0}tag{1} Release tag name. If not present, prints the total number of downloads.\n" + .format(_Text.BOLD, _Text.END)) + print("Options:\n" + " {0}-d{1}, {0}--detail{1} Print detailed statistics for release(s).\n" + " {0}-q{1}, {0}--quiet{1} Print only resulting numbers and errors.\n" + " Overrides -d option.\n" + " {0}-l{1}, {0}--latest{1} Get stats for the latest release.\n" + " Tag argument will be ignored.\n" + " {0}-h{1}, {0}--help{1} Show this help.\n" + .format(_Text.BOLD, _Text.END)) + print("Environment Variables:\n" + " {0}GITHUB_TOKEN{2} GitHub OAuth token.\n" + " Use to increase API request limit.\n" + " {1}https://github.com/settings/tokens{2}" + .format(_Text.BOLD, _Text.UNDERLINE, _Text.END)) + sys.exit(0) + + +def print_greeting(): + """ + Display greeting message. + """ + return + + +def download_stats(user=None, repo=None, tag=None, latest=False, token=None, quiet=False): + """ + Get download statistics from GitHub API. + :param user: GitHub repository owner username. If empty, user will be prompted for input. + :param repo: GitHub repository name. If empty, user will be prompted for input. + :param tag: Release tag name. If empty, get stats for all releases. + :param latest: If True, ignore "tag" parameter and get stats for the latest release. + :param token: GitHub OAuth token. If empty, API request limit will be reduced. + :param quiet: If True, print nothing. + :return: Statistics on downloads. + :raises GithubRepoError: When accessing nonexistent GitHub username, repository or release tag. + :raises GithubLimitError: When GitHub API request limit is exceeded. + :raises GithubTokenError: When trying to pass invalid GitHub API OAuth token. + :raises ConnectionError: On connection error. + """ + if not user: + user = input("GitHub Username: ") + if not repo: + repo = input("GitHub Repository: ") + if not quiet: + #print("Downloading {0}/{1} stats...".format(user, repo)) + print("Downloading PyOS info...") + url = "https://api.github.com/repos/{0}/{1}/releases".format(user, repo) + url += ("" if not tag else "/tags/" + tag) if not latest else "/latest" + headers = {} if not token else {"Authorization": "token " + token} + request = urllib2.Request(url, headers=headers) + start = time.time() + try: + response = urllib2.urlopen(request).read().decode("utf-8") + except urllib2.HTTPError as e: + if e.code == 404: + raise GithubRepoError() # Invalid GitHub username, repository or release tag. + elif e.code == 403: + raise GithubLimitError() # GitHub API request limit exceeded. + elif e.code == 401: + raise GithubTokenError() # Invalid GitHub OAuth token. + else: + raise GithubError(e.code) # Generic GitHub API exception. + except urllib2.URLError as e: + raise ConnectionError(e.reason) + stats = json.loads(response) + if not quiet: + end = time.time() + print("Downloaded in {0:.3f}s".format(end - start)) + return stats + + +def get_release_downloads(release, quiet=False): + """ + Get number of downloads for a single release. + :param release: Release statistics from GitHub API. + :param quiet: If True, print nothing. + :return: Number of downloads for a release. + """ + downloads_total = 0 + if not quiet: + encoding = sys.stdout.encoding or "ascii" + title = (release["name"] or release["tag_name"]).encode(encoding, 'ignore').decode(encoding) + print("Latest Tag: " + release["tag_name"]) + with open("latest.txt", "w") as text_file: + text_file.write("Latest Tag\n" + release["tag_name"]) + + +def get_stats_downloads(stats, quiet=False): + """ + Get number of downloads from statistics. + :param stats: Statistics from GitHub API. + :param quiet: If True, print nothing. + :return: Number of downloads. + """ + total = 0 + if isinstance(stats, dict): + total = get_release_downloads(stats, quiet) + else: + for release in stats: + total += get_release_downloads(release, quiet) + return total + + +def print_total(total, quiet=False, tag=None): + """ + Print total number of downloads. + :param total: Total number of downloads. + :param quiet: If True, print only number of downloads without an additional text. + :param tag: Release tag name (optional). + :return: Total number of downloads passed via "total" parameter. + """ + if not quiet: + print("\n{0}Total Downloads{1}: {2}" + .format(_Text.BOLD, + "" if not tag else " (" + tag + ")", + _Text.SUCCESS + str(total) + _Text.END)) + elif __name__ == "__main__" or __name__ == "ghstats.ghstats": # pragma: no cover + print(str(total)) + return total + + +def main(user=None, repo=None, tag=None, latest=False, detail=False, token=None, quiet=False): + """ + Get number of downloads for GitHub release(s). + :param user: GitHub repository owner username. If empty, user will be prompted for input. + :param repo: GitHub repository name. If empty, user will be prompted for input. + :param tag: Release tag name. If empty, get stats for all releases. + :param latest: If True, ignore "tag" parameter and get stats for the latest release. + :param detail: Detailed output containing release information. + :param token: GitHub OAuth token. If empty, API request limit will be reduced. + :param quiet: If True, print nothing. + :return: Number of downloads. + """ + if not quiet: + print_greeting() + try: + stats = download_stats(user, repo, tag, latest, token, quiet) + except GithubError as e: + error(e.message) + except ConnectionError as e: + error(str(e)) + else: + total = get_stats_downloads(stats, quiet or not detail) + # print_total(total, quiet, tag or (stats["tag_name"] if latest else None)) + return total + + +def main_cli(args=None): + """ + Parse command line arguments and pass to the main function. + :param args: Command line arguments (without script name). + :return: Number of downloads. + """ + user = None # GitHub username + repo = None # GitHub repository + tag = None # GitHub release tag + latest = False # Latest release + detail = False # Detailed output + quiet = False # Quiet output + token = get_env_token() # GitHub token + if args is None: + args = sys.argv[1:] + for arg in args: + if arg == "-q" or arg == "--quiet": + quiet = True + elif arg == "-d" or arg == "--detail": + detail = True + elif arg == "-l" or arg == "--latest": + latest = True + elif arg == "-h" or arg == "--help" or arg == "-?": + print_help() + else: + if not user: + if "/" not in arg: + user = arg + else: + userrepo = arg.split("/") + user = userrepo[0] + repo = userrepo[1] + elif not repo: + repo = arg + elif quiet and detail and latest: + break + elif not tag: + tag = arg + return main(user, repo, tag, latest, detail, token, quiet) + + +if __name__ == "__main__": # pragma: no cover + try: + main_cli(sys.argv[1:]) + except KeyboardInterrupt: + pass diff --git a/internal/update/runscript.py b/internal/update/runscript.py new file mode 100644 index 0000000..1f9fb99 --- /dev/null +++ b/internal/update/runscript.py @@ -0,0 +1,34 @@ +# PyOS +# Made for Python 2.7 +# internal/update/runscript.py + +# See ghstatscustom.py for more infomation. + +# Import custom file +import ghstatscustom +# PyOS Scripts +import internal.extra + +def getfile(): + ghstatscustom.main("Prouser123", "PyOS", None, True, True) + +# print("Getting version from text file") + +def app(): + print(internal.extra.colors.BOLD + "PyOS Update Checker" + internal.extra.colors.OKBLUE) + print("Checking for updates...") + getfile() + with open("latest.txt") as f: + print(internal.extra.colors.ENDC) + latest_version = f.read().split("\n")#[line_number] + #print(latest_version[1]) + ver = latest_version[1] + ver = ver[1:] + print("Installed Version: " + internal.extra.colors.WARNING + internal.extra.notes.ver + internal.extra.colors.ENDC) + print("Newest Version: " + internal.extra.colors.WARNING + ver + internal.extra.colors.ENDC) + if (float(internal.extra.notes.ver) >= float(ver)): + print("You are up-to-date!") + else: + print("There is a new version available.") + print("To update please visit: https://github.com/Prouser123/PyOS/releases/latest") + diff --git a/programs/__init__.py b/programs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/internal/calculator.py b/programs/calculator.py similarity index 98% rename from internal/calculator.py rename to programs/calculator.py index deadba9..34e7cbe 100644 --- a/internal/calculator.py +++ b/programs/calculator.py @@ -30,7 +30,7 @@ def multiply(x, y): def divide(x, y): return (x / y) -def start(): +def app(): internal.runCommand.commands.clear() print(internal.extra.colors.BOLD + internal.extra.notes.name + " " + appName + internal.extra.colors.ENDC) modeSelect() @@ -61,4 +61,3 @@ def calculate(mode, number1, number2): if (mode == "d"): return number1 + " / " + number2 + " = " + str(modulo.divide(float(number1), float(number2))) - \ No newline at end of file diff --git a/internal/sysinfo.py b/programs/sysinfo.py similarity index 98% rename from internal/sysinfo.py rename to programs/sysinfo.py index 95f9b3a..fc404b5 100644 --- a/internal/sysinfo.py +++ b/programs/sysinfo.py @@ -10,7 +10,7 @@ # PyOS Scripts import internal.extra -def start(): +def app(): print(internal.extra.colors.BOLD + "System Infomation" + internal.extra.colors.ENDC) # Print PyOS Version print(internal.extra.notes.name + " " + internal.extra.notes.ver + internal.extra.colors.ENDC) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6d641f8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyreadline; sys_platform == 'win32' +readline; sys_platform == 'linux2' +readline; sys_platform == 'darwin' \ No newline at end of file