aboutsummaryrefslogblamecommitdiffstats
path: root/dlpcvp.py
blob: e46ce05c4a521f5ea255b85168b0a404e5e47df6 (plain) (tree)
1
2
3
4
5
6
7
8
9
                    
                       
 
               
                   
           


              

                
               
                     
                                  
                                           
                                            


                                  


                                                                        

                                                                                 

                                                    
                         






                                                                    
 
                                   






                                                               
                                     
 
 
                                                                               


                                                                             
                                             



                                    

                                  


                                                                               




                                                    
 
 
                                              
       
                                                              
 

                                                                       

                                                   
 

                                      

                                         
                                           
                                      




                                                          
 
 























                                                                         

                                                                 
       
                                                              
 

                                               

                                                                       


                                                
 


                                               

                                         



                                           




                                                                         


                                                        
 


                                                  
                                                                      
              
 
                                                                               



                                                  










                                                                              

                                                                       




                                                                           
 
 




                                      
                                                       


                                                                      

                                                     



                                       
                          








                                                                            
#!/usr/bin/python3.6
# Requires: python3-rpm

import argparse
import configparser
import json
import logging
import os
import os.path
# import sqlite3
import sys
import tempfile
import urllib.request
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from typing import Iterable, Optional, Tuple
import xml.etree.ElementTree as ET

import rpm

# PyPI API documentation https://warehouse.readthedocs.io/api-reference/
PyPI_base = "https://pypi.org/pypi/{}/json"
# https://github.com/openSUSE/open-build-service/blob/master/docs/api/api/api.txt
# https://build.opensuse.org/apidocs/index
OBS_base = "https://api.opensuse.org"
ConfigRC = os.path.expanduser('~/.config/osc/oscrc')
CUTCHARS = len('python-')

config = configparser.ConfigParser()
config.read(ConfigRC)

logging.basicConfig(format='%(levelname)s:%(funcName)s:%(message)s',
                    level=logging.DEBUG)
log = logging.getLogger()

# or HTTPPasswordMgrWithPriorAuth ?
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
user = config[OBS_base]['user']
passw = config[OBS_base]['pass']
password_mgr.add_password(None, OBS_base, user, passw)

handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
opener = urllib.request.build_opener(handler)
urllib.request.install_opener(opener)


def get_version_from_pypi(name: str, etag: str = None) -> Tuple[str, str, str]:
    """
    For the given name of module return the latest version available on PyPI.
    """
    req = Request(url=PyPI_base.format(name))

    if etag is not None:
        req.add_header('ETag', etag)

    try:
        with urlopen(req) as resp:
            data = json.load(resp)
            info_dict = data['info']
            return info_dict['name'], info_dict['version'], resp.info()['ETag']
    except HTTPError as ex:
        if ex.getcode() == 404:
            log.error(f'Cannot find {name} on PyPI')
        else:
            raise


def suse_packages(proj: str) -> Iterable[str]:
    """
    Iterator returning names of all packages in the given proj

    ETag management won't work here, because I don't know about any way
    how to return it in iterator.
    """
    req = Request(url=OBS_base + f'/source/{proj}')

    try:
        with opener.open(req) as resp:
            raw_xml_data = ET.parse(resp)
            root = raw_xml_data.getroot()
            for elem in root.iter('entry'):
                yield elem.get('name')
    except HTTPError as ex:
        if ex.getcode() == 404:
            log.error(f'Cannot find packages for {proj}!')
        else:
            raise


def parse_spec_in_dev_null(spec_file_name, pkg_name, etag_fn, etag_spcf):
    # rpm library generates awfull lot of nonsensical goo on
    # stderr
    with open(os.devnull, 'wb') as nullf:
        old_stderr = sys.stderr
        old_stdout = sys.stdout
        sys.stderr = nullf
        sys.stdout = nullf
        try:
            spc = rpm.spec(spec_file_name)
        except Exception:
            log.error("Cannot parse {}".format(pkg_name))
        else:
            try:
                return spc.packages[0].header['Version'].decode(), \
                    etag_fn, etag_spcf
            except IndexError:
                pass
        finally:
            sys.stderr = old_stderr
            sys.stdout = old_stdout


def package_version(proj: str, pkg_name: str,
                    etag_fn: str = None, etag_spcf: str = None) \
                        -> Optional[Tuple[str, str, str]]:
    """
    Return the version of the given package in the given proj.

    Downloads SPEC file from OBS and parses it.
    """
    req_spc_name = Request(url=OBS_base + f'/source/{proj}/{pkg_name}')
    spec_files = []

    if etag_fn is not None:
        req_spc_name.add_header('ETag', etag_fn)

    try:
        with opener.open(req_spc_name) as resp:
            etag_fn = resp.info()['ETag']
            raw_xml_data = ET.parse(resp)
            root = raw_xml_data.getroot()
            for elem in root.iter('entry'):
                name = elem.get('name')
                if name.endswith('.spec'):
                    spec_files.append(name)
    except HTTPError as ex:
        if ex.getcode() == 404:
            log.error(f'Cannot accquire version of {pkg_name} in {proj}')
        else:
            raise

    if not spec_files:
        IOError(f'Cannot find SPEC file for {pkg_name}')

    try:
        spc_fname = sorted(spec_files, key=len)[0]
    except IndexError:
        log.exception(f'Cannot find correct spec_files: {spec_files}')
        return

    req_spec = Request(url=OBS_base + f'/source/{proj}/{pkg_name}/{spc_fname}')

    if etag_spcf is not None:
        req_spc_name.add_header('ETag', etag_spcf)

    try:
        with opener.open(req_spec) as resp:
            etag_spcf = resp.info()['ETag']
            spec_file_str = resp.read()
            spec_file_name = ''
            with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as spf:
                spec_file_name = spf.name
                spf.write(spec_file_str)
                spf.flush()
                os.fsync(spf.fileno())

                return parse_spec_in_dev_null(spec_file_name, pkg_name,
                                              etag_fn, etag_spcf)
    except HTTPError as ex:
        if ex.getcode() == 404:
            log.error(f'Cannot parse SPEC file {spc_fname} for {pkg_name}')
        else:
            raise


def main(prj):
    for pkg in suse_packages(prj):
        log.debug(f'pkg = {pkg}')
        if pkg.startswith('python-'):
            pypi_name = pkg[CUTCHARS:]
            pypi_ver = get_version_from_pypi(pypi_name)
            suse_ver = package_version(prj, pkg)
            # FIXME We should somehow react to the situation, not just
            # ignore it.
            if suse_ver is not None:
                print(f"{pkg} {suse_ver} {pypi_ver}")
        else:
            print(f'Is {pkg} on PyPI?')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Check available versions '
                                     'of the upstream packages on PyPI')
    parser.add_argument('--opensuse-project',
                        default='devel:languages:python:numeric',
                        help='The OpenBuildService project. Defaults '
                        'to %(default)s')

    args = parser.parse_args()
    sys.exit(main(args.opensuse_project))