| |
@@ -1,6 +1,6 @@
|
| |
# -*- coding: utf-8 -*-
|
| |
#
|
| |
- # Copyright © 2015 Red Hat, Inc.
|
| |
+ # Copyright © 2015-2019 Red Hat, Inc.
|
| |
#
|
| |
# This copyrighted material is made available to anyone wishing to use,
|
| |
# modify, copy, or redistribute it subject to the terms and conditions
|
| |
@@ -22,16 +22,27 @@
|
| |
'''
|
| |
Top level of the mdapi aiohttp application.
|
| |
'''
|
| |
- import functools
|
| |
- import json
|
| |
import logging
|
| |
import os
|
| |
|
| |
- import asyncio
|
| |
+ import aiosqlite
|
| |
import werkzeug
|
| |
+
|
| |
from aiohttp import web
|
| |
|
| |
- import mdapi.lib as mdapilib
|
| |
+ from mdapi.db import (
|
| |
+ GET_PACKAGE,
|
| |
+ GET_PACKAGE_INFO,
|
| |
+ GET_CO_PACKAGE,
|
| |
+ GET_PACKAGE_BY_SRC,
|
| |
+ GET_PACKAGE_BY,
|
| |
+ GET_FILES,
|
| |
+ GET_CHANGELOGS,
|
| |
+ Packages,
|
| |
+ Dependencies,
|
| |
+ FileList,
|
| |
+ ChangeLog
|
| |
+ )
|
| |
|
| |
|
| |
CONFIG = dict()
|
| |
@@ -46,46 +57,11 @@
|
| |
exec(compile(
|
| |
config_file.read(), os.environ['MDAPI_CONFIG'], 'exec'), CONFIG)
|
| |
|
| |
- indexfile = os.path.join(
|
| |
- os.path.dirname(os.path.abspath(__file__)), 'index.html')
|
| |
- INDEX = ''
|
| |
- with open(indexfile) as stream:
|
| |
- INDEX = stream.read()
|
| |
- INDEX = INDEX.replace('$PREFIX', CONFIG.get('PREFIX', ''))
|
| |
-
|
| |
|
| |
_log = logging.getLogger(__name__)
|
| |
|
| |
|
| |
- def allows_jsonp(function):
|
| |
- ''' Add support for JSONP queries to the endpoint decorated. '''
|
| |
-
|
| |
- @functools.wraps(function)
|
| |
- def wrapper(request, *args, **kwargs):
|
| |
- ''' Actually does the job with the arguments provided.
|
| |
-
|
| |
- :arg request: the request that was called that we want to add JSONP
|
| |
- support to
|
| |
- :type request: aiohttp.web_request.Request
|
| |
-
|
| |
- '''
|
| |
- response = yield from function(request, *args, **kwargs)
|
| |
- url_arg = request.query
|
| |
- callback = url_arg.get('callback')
|
| |
- if callback and request.method == 'GET':
|
| |
- if isinstance(callback, list):
|
| |
- callback = callback[0]
|
| |
- response.mimetype = 'application/javascript'
|
| |
- response.content_type = 'application/javascript'
|
| |
- response.text = '%s(%s);' % (callback, response.text)
|
| |
-
|
| |
- return response
|
| |
-
|
| |
- return wrapper
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def _get_pkg(branch, name=None, action=None, srcname=None):
|
| |
+ async def _get_pkg(branch, name=None, action=None, srcname=None):
|
| |
''' Return the pkg information for the given package in the specified
|
| |
branch or raise an aiohttp exception.
|
| |
'''
|
| |
@@ -95,56 +71,45 @@
|
| |
pkg = None
|
| |
wrongdb = False
|
| |
for repotype in ['updates-testing', 'updates', 'testing', None]:
|
| |
-
|
| |
- if repotype:
|
| |
- dbfile = '%s/mdapi-%s-%s-primary.sqlite' % (
|
| |
- CONFIG['DB_FOLDER'], branch, repotype)
|
| |
- else:
|
| |
- dbfile = '%s/mdapi-%s-primary.sqlite' % (
|
| |
- CONFIG['DB_FOLDER'], branch)
|
| |
+ dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}'\
|
| |
+ '-primary.sqlite'
|
| |
|
| |
if not os.path.exists(dbfile):
|
| |
wrongdb = True
|
| |
continue
|
| |
|
| |
wrongdb = False
|
| |
-
|
| |
- session = yield from mdapilib.create_session(
|
| |
- 'sqlite:///%s' % dbfile)
|
| |
- if name:
|
| |
+ async with aiosqlite.connect(f'{dbfile}') as db:
|
| |
if action:
|
| |
- pkg = yield from mdapilib.get_package_by(
|
| |
- session, action, name)
|
| |
+ # It is safe to format the query since the action does not come from the
|
| |
+ # user.
|
| |
+ query = GET_PACKAGE_BY.format(action)
|
| |
+ async with db.execute(query, (name,)) as cursor:
|
| |
+ pkg = await cursor.fetchall()
|
| |
+ if pkg:
|
| |
+ pkg = [Packages(*item) for item in pkg]
|
| |
+ break
|
| |
+ elif srcname:
|
| |
+ async with db.execute(GET_PACKAGE_BY_SRC, (srcname+'%',)) as cursor:
|
| |
+ pkg = await cursor.fetchone()
|
| |
+ if pkg:
|
| |
+ pkg = Packages(*pkg)
|
| |
+ break
|
| |
else:
|
| |
- pkg = yield from mdapilib.get_package(session, name)
|
| |
- elif srcname:
|
| |
- pkg = yield from mdapilib.get_package_by_src(session, srcname)
|
| |
- session.close()
|
| |
- if pkg:
|
| |
- break
|
| |
-
|
| |
+ async with db.execute(GET_PACKAGE, (name,)) as cursor:
|
| |
+ pkg = await cursor.fetchone()
|
| |
+ if pkg:
|
| |
+ pkg = Packages(*pkg)
|
| |
+ break
|
| |
if wrongdb:
|
| |
raise web.HTTPBadRequest()
|
| |
|
| |
if not pkg:
|
| |
raise web.HTTPNotFound()
|
| |
-
|
| |
return (pkg, repotype)
|
| |
|
| |
|
| |
- def _get_pretty(request):
|
| |
- pretty = False
|
| |
- params = request.query
|
| |
- if params.get('pretty') in ['1', 'true']:
|
| |
- pretty = True
|
| |
- # Assume pretty if html is requested and pretty is not disabled
|
| |
- elif 'text/html' in request.headers.get('ACCEPT', ''):
|
| |
- pretty = True
|
| |
- return pretty
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def _expand_pkg_info(pkgs, branch, repotype=None):
|
| |
+ async def _expand_pkg_info(pkgs, branch, repotype=None):
|
| |
''' Return a JSON blob containing all the information we want to return
|
| |
for the provided package or packages.
|
| |
'''
|
| |
@@ -155,292 +120,86 @@
|
| |
output = []
|
| |
for pkg in pkgs:
|
| |
out = pkg.to_json()
|
| |
- dbfile = '%s/mdapi-%s%s-primary.sqlite' % (
|
| |
- CONFIG['DB_FOLDER'], branch, '-%s' % repotype if repotype else '')
|
| |
-
|
| |
- session = yield from mdapilib.create_session(
|
| |
- 'sqlite:///%s' % dbfile)
|
| |
- # Fill in some extra info
|
| |
-
|
| |
- # Basic infos, always present regardless of the version of the repo
|
| |
- for datatype in ['conflicts', 'obsoletes', 'provides', 'requires']:
|
| |
- data = yield from mdapilib.get_package_info(
|
| |
- session, pkg.pkgKey, datatype.capitalize())
|
| |
- if data:
|
| |
- out[datatype] = [item.to_json() for item in data]
|
| |
+ dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}'\
|
| |
+ '-primary.sqlite'
|
| |
+
|
| |
+ async with aiosqlite.connect(f'{dbfile}') as db:
|
| |
+ # Fill in some extra info
|
| |
+ # Basic infos, always present regardless of the version of the repo
|
| |
+ for datatype in ['conflicts',
|
| |
+ 'obsoletes',
|
| |
+ 'provides',
|
| |
+ 'requires',
|
| |
+ 'enhances',
|
| |
+ 'recommends',
|
| |
+ 'suggests',
|
| |
+ 'supplements']:
|
| |
+ # It is safe to format the query since the datatype does not come from the
|
| |
+ # user.
|
| |
+ query = GET_PACKAGE_INFO.format(datatype)
|
| |
+ async with db.execute(query, (pkg.pkgKey,)) as cursor:
|
| |
+ data = await cursor.fetchall()
|
| |
+ if data:
|
| |
+ out[datatype] = [Dependencies(*item).to_json() for item in data]
|
| |
+ else:
|
| |
+ out[datatype] = data
|
| |
+
|
| |
+ # Add the list of packages built from the same src.rpm
|
| |
+ if pkg.rpm_sourcerpm:
|
| |
+ async with db.execute(GET_CO_PACKAGE, (pkg.rpm_sourcerpm,)) as cursor:
|
| |
+ copkgs = await cursor.fetchall()
|
| |
+ out['co-packages'] = list(set([
|
| |
+ cpkg[2] for cpkg in copkgs
|
| |
+ ]))
|
| |
else:
|
| |
- out[datatype] = data
|
| |
+ out['co-packages'] = []
|
| |
+ out['repo'] = repotype if repotype else 'release'
|
| |
+ output.append(out)
|
| |
|
| |
- # New meta-data present for soft dependency management in RPM
|
| |
- for datatype in [
|
| |
- 'enhances', 'recommends', 'suggests', 'supplements']:
|
| |
- data = yield from mdapilib.get_package_info(
|
| |
- session, pkg.pkgKey, datatype.capitalize())
|
| |
- if data:
|
| |
- out[datatype] = [item.to_json() for item in data]
|
| |
- else:
|
| |
- out[datatype] = data
|
| |
-
|
| |
- # Add the list of packages built from the same src.rpm
|
| |
- if pkg.rpm_sourcerpm:
|
| |
- copkgs = yield from mdapilib.get_co_packages(
|
| |
- session, pkg.rpm_sourcerpm)
|
| |
- out['co-packages'] = list(set([
|
| |
- cpkg.name for cpkg in copkgs
|
| |
- ]))
|
| |
- else:
|
| |
- out['co-packages'] = []
|
| |
- out['repo'] = repotype if repotype else 'release'
|
| |
- session.close()
|
| |
- output.append(out)
|
| |
if singleton:
|
| |
return output[0]
|
| |
else:
|
| |
return output
|
| |
|
| |
|
| |
- @asyncio.coroutine
|
| |
- @allows_jsonp
|
| |
- def get_pkg(request):
|
| |
- _log.info('get_pkg %s', request)
|
| |
- branch = request.match_info.get('branch')
|
| |
- pretty = _get_pretty(request)
|
| |
- name = request.match_info.get('name')
|
| |
- pkg, repotype = yield from _get_pkg(branch, name)
|
| |
-
|
| |
- output = yield from _expand_pkg_info(pkg, branch, repotype)
|
| |
-
|
| |
- args = {}
|
| |
- if pretty:
|
| |
- args = dict(sort_keys=True, indent=4, separators=(',', ': '))
|
| |
-
|
| |
- output = web.Response(
|
| |
- body=json.dumps(output, **args).encode('utf-8'),
|
| |
- content_type='application/json')
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- @allows_jsonp
|
| |
- def get_src_pkg(request):
|
| |
- _log.info('get_src_pkg %s', request)
|
| |
- branch = request.match_info.get('branch')
|
| |
- pretty = _get_pretty(request)
|
| |
- name = request.match_info.get('name')
|
| |
- pkg, repotype = yield from _get_pkg(branch, srcname=name)
|
| |
-
|
| |
- output = yield from _expand_pkg_info(pkg, branch, repotype)
|
| |
-
|
| |
- args = {}
|
| |
- if pretty:
|
| |
- args = dict(sort_keys=True, indent=4, separators=(',', ': '))
|
| |
-
|
| |
- return web.Response(
|
| |
- body=json.dumps(output, **args).encode('utf-8'),
|
| |
- content_type='application/json')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- @allows_jsonp
|
| |
- def get_pkg_files(request):
|
| |
- _log.info('get_pkg_files %s', request)
|
| |
- branch = request.match_info.get('branch')
|
| |
- name = request.match_info.get('name')
|
| |
- pretty = _get_pretty(request)
|
| |
- pkg, repotype = yield from _get_pkg(branch, name)
|
| |
-
|
| |
- dbfile = '%s/mdapi-%s%s-filelists.sqlite' % (
|
| |
- CONFIG['DB_FOLDER'], branch, '-%s' % repotype if repotype else '')
|
| |
+ async def _get_files(pkg_id, branch, repotype):
|
| |
+ ''' Return the files list for the given package in the specified
|
| |
+ branch.
|
| |
+ '''
|
| |
+ dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}'\
|
| |
+ '-filelists.sqlite'
|
| |
if not os.path.exists(dbfile):
|
| |
raise web.HTTPBadRequest()
|
| |
|
| |
- session2 = yield from mdapilib.create_session(
|
| |
- 'sqlite:///%s' % dbfile)
|
| |
- filelist = yield from mdapilib.get_files(session2, pkg.pkgId)
|
| |
- session2.close()
|
| |
+ async with aiosqlite.connect(f"{dbfile}") as db:
|
| |
+ async with db.execute(GET_FILES, (pkg_id,)) as cursor:
|
| |
+ filelists = await cursor.fetchall()
|
| |
+
|
| |
+ filelists = [FileList(*item) for item in filelists]
|
| |
|
| |
output = {
|
| |
- 'files': [fileinfo.to_json() for fileinfo in filelist],
|
| |
+ 'files': [fileinfo.to_json() for fileinfo in filelists],
|
| |
'repo': repotype if repotype else 'release',
|
| |
}
|
| |
- args = {}
|
| |
- if pretty:
|
| |
- args = dict(sort_keys=True, indent=4, separators=(',', ': '))
|
| |
-
|
| |
- return web.Response(
|
| |
- body=json.dumps(output, **args).encode('utf-8'),
|
| |
- content_type='application/json')
|
| |
-
|
| |
+ return output
|
| |
|
| |
- @asyncio.coroutine
|
| |
- @allows_jsonp
|
| |
- def get_pkg_changelog(request):
|
| |
- _log.info('get_pkg_changelog %s', request)
|
| |
- branch = request.match_info.get('branch')
|
| |
- name = request.match_info.get('name')
|
| |
- pretty = _get_pretty(request)
|
| |
- pkg, repotype = yield from _get_pkg(branch, name)
|
| |
|
| |
- dbfile = '%s/mdapi-%s%s-other.sqlite' % (
|
| |
- CONFIG['DB_FOLDER'], branch, '-%s' % repotype if repotype else '')
|
| |
+ async def _get_changelog(pkg_id, branch, repotype):
|
| |
+ ''' Return the changelog for the given package in the specified
|
| |
+ branch.
|
| |
+ '''
|
| |
+ dbfile = f'{CONFIG["DB_FOLDER"]}/mdapi-{branch}{"-"+repotype if repotype else ""}-other.sqlite'
|
| |
if not os.path.exists(dbfile):
|
| |
raise web.HTTPBadRequest()
|
| |
|
| |
- session2 = yield from mdapilib.create_session(
|
| |
- 'sqlite:///%s' % dbfile)
|
| |
- changelogs = yield from mdapilib.get_changelog(session2, pkg.pkgId)
|
| |
- session2.close()
|
| |
+ async with aiosqlite.connect(f"{dbfile}") as db:
|
| |
+ async with db.execute(GET_CHANGELOGS, (pkg_id,)) as cursor:
|
| |
+ changelogs = await cursor.fetchall()
|
| |
+
|
| |
+ changelogs = [ChangeLog(*item) for item in changelogs]
|
| |
|
| |
output = {
|
| |
'changelogs': [changelog.to_json() for changelog in changelogs],
|
| |
'repo': repotype if repotype else 'release',
|
| |
}
|
| |
- args = {}
|
| |
- if pretty:
|
| |
- args = dict(sort_keys=True, indent=4, separators=(',', ': '))
|
| |
-
|
| |
- return web.Response(
|
| |
- body=json.dumps(output, **args).encode('utf-8'),
|
| |
- content_type='application/json')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def list_branches(request):
|
| |
- ''' Return the list of all branches currently supported by mdapi
|
| |
- '''
|
| |
- _log.info('list_branches: %s', request)
|
| |
- pretty = _get_pretty(request)
|
| |
- output = sorted(list(set([
|
| |
- # Remove the front part `mdapi-` and the end part -<type>.sqlite
|
| |
- filename.replace('mdapi-', '').rsplit('-', 2)[0].replace(
|
| |
- '-updates', '')
|
| |
- for filename in os.listdir(CONFIG['DB_FOLDER'])
|
| |
- if filename.startswith('mdapi') and filename.endswith('.sqlite')
|
| |
- ])))
|
| |
-
|
| |
- args = {}
|
| |
- if pretty:
|
| |
- args = dict(sort_keys=True, indent=4, separators=(',', ': '))
|
| |
-
|
| |
- response = web.Response(body=json.dumps(output, **args).encode('utf-8'),
|
| |
- content_type='application/json')
|
| |
-
|
| |
- # The decorator doesn't work for this endpoint, so do it manually here
|
| |
- # I am not really sure what doesn't work but it seems this endpoint is
|
| |
- # returning an object instead of the expected generator despite it being
|
| |
- # flagged as an asyncio coroutine
|
| |
- url_arg = request.query
|
| |
- callback = url_arg.get('callback')
|
| |
- if callback and request.method == 'GET':
|
| |
- if isinstance(callback, list):
|
| |
- callback = callback[0]
|
| |
- response.mimetype = 'application/javascript'
|
| |
- response.content_type = 'application/javascript'
|
| |
- response.text = '%s(%s);' % (callback, response.text)
|
| |
-
|
| |
- return response
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- @allows_jsonp
|
| |
- def process_dep(request, action):
|
| |
- ''' Return the information about the packages having the specified
|
| |
- action (provides, requires, obsoletes...)
|
| |
- '''
|
| |
- _log.info('process_dep %s: %s', action, request)
|
| |
- branch = request.match_info.get('branch')
|
| |
- pretty = _get_pretty(request)
|
| |
- name = request.match_info.get('name')
|
| |
-
|
| |
- try:
|
| |
- pkg, repotype = yield from _get_pkg(branch, name, action=action)
|
| |
- except:
|
| |
- raise web.HTTPBadRequest()
|
| |
-
|
| |
- output = yield from _expand_pkg_info(pkg, branch, repotype)
|
| |
-
|
| |
- args = {}
|
| |
- if pretty:
|
| |
- args = dict(sort_keys=True, indent=4, separators=(',', ': '))
|
| |
-
|
| |
- return web.Response(body=json.dumps(output, **args).encode('utf-8'),
|
| |
- content_type='application/json')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_provides(request):
|
| |
- return process_dep(request, 'provides')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_requires(request):
|
| |
- return process_dep(request, 'requires')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_obsoletes(request):
|
| |
- return process_dep(request, 'obsoletes')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_conflicts(request):
|
| |
- return process_dep(request, 'conflicts')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_enhances(request):
|
| |
- return process_dep(request, 'enhances')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_recommends(request):
|
| |
- return process_dep(request, 'recommends')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_suggests(request):
|
| |
- return process_dep(request, 'suggests')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def get_supplements(request):
|
| |
- return process_dep(request, 'supplements')
|
| |
-
|
| |
-
|
| |
- @asyncio.coroutine
|
| |
- def index(request):
|
| |
- _log.info('index %s', request)
|
| |
- return web.Response(
|
| |
- body=INDEX.encode('utf-8'),
|
| |
- content_type='text/html',
|
| |
- charset='utf-8')
|
| |
-
|
| |
-
|
| |
- def _set_routes(app):
|
| |
- routes = []
|
| |
- prefix = CONFIG.get('PREFIX', '')
|
| |
- if prefix:
|
| |
- routes.append(('', index))
|
| |
-
|
| |
- routes.extend([
|
| |
- ('/', index),
|
| |
- ('/branches', list_branches),
|
| |
- ('/{branch}/pkg/{name}', get_pkg),
|
| |
- ('/{branch}/srcpkg/{name}', get_src_pkg),
|
| |
-
|
| |
- ('/{branch}/provides/{name}', get_provides),
|
| |
- ('/{branch}/requires/{name}', get_requires),
|
| |
- ('/{branch}/obsoletes/{name}', get_obsoletes),
|
| |
- ('/{branch}/conflicts/{name}', get_conflicts),
|
| |
-
|
| |
- ('/{branch}/enhances/{name}', get_enhances),
|
| |
- ('/{branch}/recommends/{name}', get_recommends),
|
| |
- ('/{branch}/suggests/{name}', get_suggests),
|
| |
- ('/{branch}/supplements/{name}', get_supplements),
|
| |
-
|
| |
- ('/{branch}/files/{name}', get_pkg_files),
|
| |
- ('/{branch}/changelog/{name}', get_pkg_changelog),
|
| |
- ])
|
| |
- for route in routes:
|
| |
- app.router.add_route('GET', prefix + route[0], route[1])
|
| |
- return app
|
| |
+ return output
|
| |
This commit removes the dependency to sqlalchemy to manage the
sqlite connection and queries. The reason behind is that sqlalchemy
is blocking the event loop in an asyncio context which made each
request needed to have access to the sqlite databases blocking.
The commit replaces sqlalchemy by aiosqlite a async sqlite wrapper
around the sqlite3 module from the standard library.
In order to make it easier to understand the code base, all the
queries used and the classes used to store the queries results
were moved to the mdapi/db.py module.
The views are now stored in the mdapi.views.py and added to the
application router in mdapi/server.py.
The logic which forms the response to the requests based on the
data gathered in the databases is present in mdapi/init.py.
It might make sense to move this to the db.py module in a later
commit.
Before this commit mdapi answered an average of 28.5 requests
per seconds for 100 concurent requests.
After this commit mdapi answers an average of 97 requests
per seconds for 100 concurent requests.
Signed-off-by: Clement Verna cverna@tutanota.com