#!/usr/bin/env python3 # -*- coding: utf-8 -*- """The setup script.""" from collections import namedtuple from html.parser import HTMLParser import os import pkg_resources from setuptools import setup, find_packages # noqa from setuptools.command.develop import develop # noqa from setuptools.command.install import install # noqa from setuptools.command.bdist_egg import bdist_egg # noqa import sys import tarfile import typing as T from urllib.error import HTTPError, URLError from urllib.request import urlopen from wheel.bdist_wheel import bdist_wheel # noqa import zipfile GECKO_RELEASE_PATH = "https://github.com/mozilla/geckodriver" PKG_NAME = "bot_z" VERSION = "0.2.0" AUTHOR = "blallo" AUTHOR_EMAIL = "blallo@autistici.org" BIN_PATH = "bin/geckodriver" with open("README.md") as readme_file: readme = readme_file.read() requirements = [ "Click>=7.0", "selenium>=3.141.0", "lockfile>=0.12.2", "python-daemon>=2.2.3", ] setup_requirements = [] # type: T.List[str] test_requirements = [] # type: T.List[str] class GitTags(HTMLParser): tags: T.List[str] = list() take_next = 0 def handle_starttag(self, tag, attrs): dattrs = dict(attrs) if "commit-title" in dattrs.get("class", ""): self.take_next = 1 def handle_data(self, data): if self.take_next == 0: return elif self.take_next == 1: self.take_next = 2 elif self.take_next == 2: self.tags.append(data.strip("\n").strip(" ").strip("\n")) self.take_next = 0 def retrieve_page(url: str) -> bytes: """ Auxiliary function to download html body from and URI, handling the errors. """ try: with urlopen(url) as conn: content = conn.read() except HTTPError as e: print("Connection error: {!s}".format(e)) raise except URLError as e: print("Check the URI: {!s}".format(e)) raise return content def find_latest_version(url: str) -> str: """ Retrieves latest geckodriver tag. """ tag_page = retrieve_page("{}/tags".format(url)) gt = GitTags() gt.feed(tag_page.decode("utf-8")) gt.tags.sort() return gt.tags[-1] def verify_if_superuser() -> bool: """ Checks if uid or euid is 0. """ _uid = os.getuid() _euid = os.geteuid() return _uid == 0 or _euid == 0 def ensure_local_folder() -> None: """ Create a bin/ folder in the current package installation path. """ bin_path = pkg_resources.resource_filename(PKG_NAME, BIN_PATH) print("[LOCAL_FOLDER] ensuring local folder: {}".format(bin_path)) pkg_resources.ensure_directory(bin_path) def assemble_driver_uri( version: T.Optional[str] = None, platform: T.Optional[str] = None ) -> str: """ Selects the right geckodriver URI. """ # TODO: use pkg_resources.get_platform() if not version: version = find_latest_version(GECKO_RELEASE_PATH) if not platform: s_platform = sys.platform is_64bits = sys.maxsize > 2 ** 32 if is_64bits: platform = "{}64".format(s_platform) else: platform = "{}64".format(s_platform) if "win" in platform: ext = "zip" else: ext = "tar.gz" return "{base}/releases/download/{vers}/geckodriver-{vers}-{platform}.{ext}".format( base=GECKO_RELEASE_PATH, vers=version, platform=platform, ext=ext ) def download_driver_bin(uri: str, path: str) -> None: """ Donwloads the geckodriver binary. """ name = uri.split("/")[-1] filepath = os.path.join(path, name) print("[DRIVER] downloading '{}' to {}".format(uri, filepath)) content = retrieve_page(uri) try: with open(filepath, "wb") as f: f.write(content) if name.endswith(".zip"): with zipfile.ZipFile(filepath, "r") as z: z.extractall(path) elif name.endswith(".tar.gz"): with tarfile.open(filepath, "r") as r: r.extractall(path) else: raise RuntimeError("Unrecognized file extension: %s", name) finally: os.remove(filepath) def preinstall(platform: T.Optional[str] = None) -> None: """ Performs all the postintallation flow, donwloading in the right place the geckodriver binary. """ # target_path = os.path.join(os.path.abspath(os.path.curdir), 'bot_z', 'bin') target_path = pkg_resources.resource_filename("bot_z", "bin") ensure_local_folder() version = os.environ.get("BOTZ_GECKO_VERSION") gecko_uri = assemble_driver_uri(version, platform) print("[POSTINSTALL] gecko_uri: {}".format(gecko_uri)) download_driver_bin(gecko_uri, target_path) def translate_platform_to_gecko_vers(plat: str) -> str: """ Map appropriately the platform provided on the command line to the one used by PEP 513. """ if plat is None: return None PLATS = { "win32": "win32", "win-amd64": "win64", "manylinux1-i686": "linux32", "manylinux1-x86_64": "linux64", "macosx": "macos", } try: return PLATS[plat] except KeyError as e: print("Allowed platforms are: {!r}".format(list(PLATS.keys()))) raise # From: https://stackoverflow.com/a/36902139 class CustomDevelopCommand(develop): """Custom installation for development mode.""" def run(self): print("POSTINSTALL") preinstall() super().run() class CustomInstallCommand(install): """Custom installation for installation mode.""" def run(self): opts = self.distribution.get_cmdline_options() platform = None if "bdist_wheel" in opts: platform = translate_platform_to_gecko_vers( opts["bdist_wheel"].get("plat-name") ) preinstall(platform) super().run() # From: https://stackoverflow.com/a/45150383 class CustomBDistWheel(bdist_wheel): """ Custom bdist_wheel command to ship the right binary of geckodriver. """ def finalize_options(self): super().finalize_options() self.root_is_pure = False def get_tag(self): python, abi, plat = super().get_tag() python, abi = "py3", "none" return python, abi, plat setup( name=PKG_NAME, version=VERSION, description="A bot to easen the daily routine with zucchetti virtual badge.", long_description=readme, author=AUTHOR, author_email=AUTHOR_EMAIL, url="https://git.abbiamoundominio.org/blallo/BotZ", packages=find_packages(include=["bot_z"]), cmdclass={ "develop": CustomDevelopCommand, "install": CustomInstallCommand, "bdist_wheel": CustomBDistWheel, }, entry_points={"console_scripts": ["bot_z=bot_z.cli:cli"]}, package_data={"bot_z": ["bin/geckodriver"]}, include_package_data=True, install_requires=requirements, license="GLWTS Public Licence", zip_safe=False, keywords="bot_z", classifiers=[ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "License :: GLWTS Public Licence", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", ], test_suite="pytest", tests_require=test_requirements, setup_requires=setup_requirements, )