#!/usr/bin/env python3 # -*- coding: utf-8 -*- """The setup script.""" from collections import namedtuple from distutils.dir_util import copy_tree, mkpath import glob from html.parser import HTMLParser import json import os import pkg_resources from pprint import pformat from setuptools import setup, find_packages # type: ignore from setuptools.command.develop import develop # type: ignore from setuptools.command.install import install # type: ignore from setuptools.command.bdist_egg import bdist_egg # type: ignore import shutil import subprocess 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 # type: ignore import zipfile GECKO_RELEASE_PATH = "https://github.com/mozilla/geckodriver" PKG_NAME = "bot_z" VERSION = "1.1.3" AUTHOR = "blallo" AUTHOR_EMAIL = "blallo@autistici.org" BIN_PATH = "bin/geckodriver" ASSET_FILE_PATHS = [ "assets/*", "assets/static/*", "assets/static/css/*", "assets/static/js/*", ] 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", "aiohttp>=3.5.4", "aiodns>=2.0.0", "cchardet", "aiohttp-session==2.7.0", "cryptography==2.7", "PyYAML==5.1.2", "passlib>=1.7.1, <2.0.0", "bcrypt>=3.0.0", ] setup_requirements = [] # type: T.List[str] test_requirements = [] # type: T.List[str] def _find_javascript_pkgmgr(): pkgmgr_list = ("yarn", "npm") for _pkgmgr in pkgmgr_list: if shutil.which(_pkgmgr): pkgmgr = _pkgmgr break if not pkgmgr: raise RuntimeError( "Missing javascript package manager. Allowed are: {}".format(pkgmgr_list) ) return pkgmgr def _find_built_assets(base_path: str): asset_manifest = os.path.join(base_path, "build", "asset-manifest.json") with open(asset_manifest) as f: manifest = json.load(f) return manifest.get("files") def build_web(): pkgmgr = _find_javascript_pkgmgr() webpath = "./bot.z_web" # TODO: find base path script = f"cd {webpath} && {pkgmgr} build" print("[BUILD_WEB] running script:\n\t{}".format(script)) subprocess.check_call(["bash", "-c", script]) assets = _find_built_assets(webpath) print("[BUILD_WEB] built assets: {}".format(pformat(assets))) assets_path = pkg_resources.resource_filename("api", "assets") if not os.path.exists(assets_path): mkpath(assets_path) copy_tree(os.path.join(webpath, "build"), assets_path) 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) PLATFORM_MAP = { "win32": "win32", "win64": "win64", "linux32": "linux32", "linux64": "linux64", "darwin64": "macos", "darwin32": "macos", # This is impossible (?) "macos": "macos", } def _identify_platform(): s_platform = sys.platform if s_platform == "darwin": return "macos" is_64bits = sys.maxsize > 2 ** 32 if "linux" in s_platform: plat = "linux" elif "win" in s_platform: plat = "win" if is_64bits: platform = "{}64".format(plat) else: platform = "{}32".format(plat) return platform 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 platform is None: platform = _identify_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_MAP[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.Text, build_iface: bool = True) -> None: """ Performs all the postintallation flow, donwloading in the right place the geckodriver binary. """ target_path = pkg_resources.resource_filename( "bot_z", os.path.join("bin", platform) ) if not os.path.exists(target_path): mkpath(target_path) 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) if build_iface: build_web() PLATS = { "win32": "win32", "win-amd64": "win64", "manylinux1-i686": "linux32", "manylinux1-x86_64": "linux64", "macosx": "macos", } 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 try: return PLATS[plat] except KeyError: 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") platform = _identify_platform() preinstall(platform, build_iface=False) 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", "api"]), cmdclass={ "develop": CustomDevelopCommand, "install": CustomInstallCommand, "bdist_wheel": CustomBDistWheel, }, entry_points={"console_scripts": ["bot_z=bot_z.cli:cli", "z_app=api.app:cli"]}, package_data={"bot_z": ["bin/geckodriver"], "api": ASSET_FILE_PATHS}, 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, )