diff --git a/src/relengapi_clobberer/relengapi_clobberer/__init__.py b/src/relengapi_clobberer/relengapi_clobberer/__init__.py index 127232f5..fadfe22b 100644 --- a/src/relengapi_clobberer/relengapi_clobberer/__init__.py +++ b/src/relengapi_clobberer/relengapi_clobberer/__init__.py @@ -4,10 +4,21 @@ from __future__ import absolute_import -from relengapi_common import create_app, db -from relengapi_clobberer import _app +import os + +from relengapi_common import create_app, db + + +here = os.path.dirname(__file__) + +def init_app(app): + app.api.register( + os.path.join(here, "swagger.yml"), + base_url=app.config.get('CLOBBERER_BASE_URL'), + ) + +app = create_app(__name__, [db, init_app]) -app = create_app(__name__, [db, _app]) if __name__ == '__main__': app.run(debug=True) diff --git a/src/relengapi_clobberer/relengapi_clobberer/_app.py b/src/relengapi_clobberer/relengapi_clobberer/_app.py deleted file mode 100644 index d2fd3b24..00000000 --- a/src/relengapi_clobberer/relengapi_clobberer/_app.py +++ /dev/null @@ -1,37 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -from __future__ import absolute_import - -from flask_restplus import Resource - - -class App: - pass - - -def init_app(app): - - clobberer = App() - - @app.api.route('/buildbot') - class Buildbot(Resource): - - def get(self): - return {'hello': 'world'} - - def post(self): - return {'hello': 'world'} - - @app.api.route('/taskcluster') - class Taskcluster(Resource): - - def get(self): - return {'hello': 'world'} - - def post(self): - return {'hello': 'world'} - - - return clobberer diff --git a/src/relengapi_clobberer/relengapi_clobberer/_app.py.old b/src/relengapi_clobberer/relengapi_clobberer/_app.py.old deleted file mode 100644 index 0d1301bb..00000000 --- a/src/relengapi_clobberer/relengapi_clobberer/_app.py.old +++ /dev/null @@ -1,134 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -from __future__ import absolute_import - -import time -import re -import datetime -import wsme.types - -from flask import g -from flask_login import current_user - -from relengapi_common.api import apimethod -from relengapi_clobberer import api -from relengapi_clobberer.models import DB_DECLARATIVE_BASE, ClobbererTimes - - -__name__ = 'clobberer' - - -def _add_clobber(app, session, branch, builddir, slave=None): - """ - A common method for adding clobber times to a session. The session passed - in is returned; but is only committed if the commit option is True. - """ - match = re.search('^' + api.BUILDBOT_BUILDDIR_REL_PREFIX + '.*', builddir) - if match is None: - try: - who = current_user.authenticated_email - except AttributeError: - if current_user.anonymous: - who = 'anonymous' - else: - # TokenUser doesn't show up as anonymous; but also has no - # authenticated_email - who = 'automation' - - clobberer_times = ClobbererTimes.as_unique( - session, - branch=branch, - builddir=builddir, - slave=slave, - ) - clobberer_times.lastclobber = int(time.time()) - clobberer_times.who = who - session.add(clobberer_times) - return None - app.log.debug('Rejecting clobber of builddir with release ' - 'prefix: {}'.format(builddir)) - return None - - -class Branch(wsme.types.Base): - """Represents branches of buildbot - """ - name = wsme.types.wsattr(unicode, mandatory=True) - data = wsme.types.wsattr( - {unicode: [unicode]}, mandatory=False, default=list()) - - -def init_app(app): - - caches_to_skip = app.config.get('TASKCLUSTER_CACHES_TO_SKIP', []) - - @app.route('/') - def root(): - # TODO: point to tools page for clobberer or documentation - return 'Clobberer is running ...' - - @app.route('/buildbot', methods=['GET']) - @apimethod([Branch]) - def get_buildout(): - """List of all buildbot branches. - """ - session = g.db.session(DB_DECLARATIVE_BASE) - # TODO: only cache this in production - # branches = app.cache.cached()(api.buildbot_branches)(session) - branches = api.buildbot_branches(session) - return [ - Branch( - name=branch['name'], - data={ - name: [ - datetime.datetime.fromtimestamp( - builder.lastclobber).strftime("%Y-%m-%d %H:%M:%S") - for builder in builders - if builder.lastclobber - ] - for name, builders in branch['builders'].items() - } - ) - for branch in branches - ] - - @app.route('/buildbot', methods=['POST']) - @apimethod(unicode, body=[unicode, unicode]) - def post_buildout(body): - """ - Request clobbers for particular branches and builddirs. - """ - session = g.db.session(DB_DECLARATIVE_BASE) - for clobber in body: - _add_clobber( - app, - session, - branch=clobber.branch, - builddir=clobber.builddir, - slave=clobber.slave - ) - session.commit() - return None - - @app.route('/taskcluster', methods=['GET']) - @apimethod([Branch]) - def get_taskcluster(): - """List of all the gecko branches with their worker types - """ - branches = app.cache.cached()(api.taskcluster_branches)() - return [ - Branch( - name=branchName, - data={ - workerName: filter(lambda x: x not in caches_to_skip, worker['caches']) # noqa - for workerName, worker in branch['workerTypes'].items() - } - ) - for branchName, branch in branches.items() - ] - - # TODO post_taskcluster - - return api diff --git a/src/relengapi_clobberer/relengapi_clobberer/api.py b/src/relengapi_clobberer/relengapi_clobberer/api.py index 15e31331..33a661aa 100644 --- a/src/relengapi_clobberer/relengapi_clobberer/api.py +++ b/src/relengapi_clobberer/relengapi_clobberer/api.py @@ -4,131 +4,61 @@ from __future__ import absolute_import -import taskcluster - -from sqlalchemy import and_ -from sqlalchemy import func -from sqlalchemy import not_ - -from relengapi_clobberer.models import ClobbererBuilds -from relengapi_clobberer.models import ClobbererTimes +from flask import g, current_app +from relengapi_clobberer import models -BUILDBOT_BUILDDIR_REL_PREFIX = 'rel-' -BUILDBOT_BUILDER_REL_PREFIX = 'release-' -TASKCLUSTER_DECISION_NAMESPACE = 'gecko.v2.%s.latest.firefox.decision' +def get_buildbot(): + return models.buildbot_branches(g.db.session) -def buildbot_branches(session): - """List of all buildbot branches. - """ +def post_buildbot(body): + result = [] - branches = session.query(ClobbererBuilds.branch).distinct() - - # Users shouldn't see any branch associated with a release builddir - branches = branches.filter(not_( - ClobbererBuilds.builddir.startswith(BUILDBOT_BUILDDIR_REL_PREFIX))) - - branches = branches.order_by(ClobbererBuilds.branch) - - return [dict(name=branch[0], - builders=buildbot_branch_summary(session, branch[0])) - for branch in branches] - - -def buildbot_branch_summary(session, branch): - """Return a dictionary of most recent ClobbererTimess grouped by - buildername. - """ - # Isolates the maximum lastclobber for each builddir on a branch - max_ct_sub_query = session.query( - func.max(ClobbererTimes.lastclobber).label('lastclobber'), - ClobbererTimes.builddir, - ClobbererTimes.branch - ).group_by( - ClobbererTimes.builddir, - ClobbererTimes.branch - ).filter(ClobbererTimes.branch == branch).subquery() - - # Finds the "greatest n per group" by joining with the - # max_ct_sub_query - # This is necessary to get the correct "who" values - sub_query = session.query(ClobbererTimes).join(max_ct_sub_query, and_( - ClobbererTimes.builddir == max_ct_sub_query.c.builddir, - ClobbererTimes.lastclobber == max_ct_sub_query.c.lastclobber, - ClobbererTimes.branch == max_ct_sub_query.c.branch)).subquery() - - # Attaches builddirs, along with their max lastclobber to a - # buildername - full_query = session.query( - ClobbererBuilds.buildername, - ClobbererBuilds.builddir, - sub_query.c.lastclobber, - sub_query.c.who - ).outerjoin( - sub_query, - ClobbererBuilds.builddir == sub_query.c.builddir, - ).filter( - ClobbererBuilds.branch == branch, - not_(ClobbererBuilds.buildername.startswith(BUILDBOT_BUILDER_REL_PREFIX)) # noqa - ).distinct().order_by(ClobbererBuilds.buildername) - - summary = dict() - for result in full_query: - buildername, builddir, lastclobber, who = result - summary.setdefault(buildername, []) - summary[buildername].append( - ClobbererTimes( - branch=branch, - builddir=builddir, - lastclobber=lastclobber, - who=who + try: + for clobber in body: + result.append( + models.clobber_buildbot( + g.db.session, + branch=clobber['branch'], + builddir=clobber['builddir'], + slave=clobber['slave'] + ) ) - ) - return summary + g.db.session.commit() + + except Exception as e: + g.db.session.rollback() + return dict(error=str(e.message)) + + return result -def taskcluster_branches(): - """Dict of workerTypes per branch with their respected workerTypes - """ - index = taskcluster.Index() - queue = taskcluster.Queue() +def get_taskcluster(): + caches_to_skip = current_app.config.get('TASKCLUSTER_CACHES_TO_SKIP', []) + return models.taskcluster_branches(caches_to_skip) - result = index.listNamespaces('gecko.v2', dict(limit=1000)) - branches = { - i['name']: dict(name=i['name'], workerTypes=dict()) - for i in result.get('namespaces', []) - } +def post_taskcluster(): + # TODO: need to make this route work + credentials = [] - for branchName, branch in branches.items(): + # XXX: it should get authenticated via Authenticated header + client_id = current_app.config.get('TASKCLUSTER_CLIENT_ID') + access_token = current_app.config.get('TASKCLUSTER_ACCESS_TOKEN') - # decision task might not exist - try: - decision_task = index.findTask( - TASKCLUSTER_DECISION_NAMESPACE % branchName) - decision_graph = queue.getLatestArtifact( - decision_task['taskId'], 'public/graph.json') - except taskcluster.exceptions.TaskclusterRestFailure: - continue + if client_id and access_token: + credentials = [dict( + credentials=dict( + clientId=client_id, + accessToken=access_token, + ))] - for task in decision_graph.get('tasks', []): - task = task['task'] - task_cache = task.get('payload', dict()).get('cache', dict()) + purge_cache = taskcluster.PurgeCache(*credentials) - provisionerId = task.get('provisionerId') - if provisionerId: - branch['provisionerId'] = provisionerId + for item in body: + purge_cache.purgeCache(item.provisionerId, + item.workerType, + dict(cacheName=item.cacheName)) - workerType = task.get('workerType') - if workerType: - branch['workerTypes'].setdefault( - workerType, dict(name=workerType, caches=[])) - - if len(task_cache) > 0: - branch['workerTypes'][workerType]['caches'] = list(set( - branch['workerTypes'][workerType]['caches'] + - task_cache.keys() - )) - - return branches + return None diff --git a/src/relengapi_clobberer/relengapi_clobberer/models.py b/src/relengapi_clobberer/relengapi_clobberer/models.py index 7a585510..417b0a12 100644 --- a/src/relengapi_clobberer/relengapi_clobberer/models.py +++ b/src/relengapi_clobberer/relengapi_clobberer/models.py @@ -4,34 +4,34 @@ from __future__ import absolute_import -import time + import sqlalchemy as sa +import taskcluster as tc +import time -from relengapi_common import db - -DB_DECLARATIVE_BASE = 'clobberer' +from relengapi_common.db import db -class ClobbererBase(db.declarative_base(DB_DECLARATIVE_BASE)): - __abstract__ = True - - id = sa.Column(sa.Integer, primary_key=True) - branch = sa.Column(sa.String(50), index=True) - builddir = sa.Column(sa.String(100), index=True) +BUILDBOT_BUILDDIR_REL_PREFIX = 'rel-' +BUILDBOT_BUILDER_REL_PREFIX = 'release-' +TASKCLUSTER_DECISION_NAMESPACE = 'gecko.v2.%s.latest.firefox.decision' -class ClobbererBuilds(ClobbererBase, db.UniqueMixin): +class Build(db.Model): """ A clobberable builds. """ __tablename__ = 'clobberer_builds' + id = sa.Column(sa.Integer, primary_key=True) + branch = sa.Column(sa.String(50), index=True) + builddir = sa.Column(sa.String(100), index=True) buildername = sa.Column(sa.String(100)) last_build_time = sa.Column( sa.Integer, nullable=False, - default=int(time.time()) + default=lambda: int(time.time()) ) @classmethod @@ -48,17 +48,18 @@ class ClobbererBuilds(ClobbererBase, db.UniqueMixin): ) -class ClobbererTimes(ClobbererBase, db.UniqueMixin): - """ - A clobber request. - """ +class ClobberTime(db.Model): __tablename__ = 'clobberer_times' __table_args__ = ( # Index to speed up lastclobber lookups sa.Index('ix_get_clobberer_times', 'slave', 'builddir', 'branch'), ) + + id = sa.Column(sa.Integer, primary_key=True) + branch = sa.Column(sa.String(50), index=True) slave = sa.Column(sa.String(30), index=True) + builddir = sa.Column(sa.String(100), index=True) lastclobber = sa.Column( sa.Integer, nullable=False, @@ -78,3 +79,180 @@ class ClobbererTimes(ClobbererBase, db.UniqueMixin): cls.slave == slave, cls.builddir == builddir, ) + + +def buildbot_branches(db_session): + """List of all buildbot branches. + """ + + branches = db_session.query( + Build.branch + ).filter( + # Users shouldn't see any branch associated with a release builddir + sa.not_( + Build.builddir.startswith(BUILDBOT_BUILDDIR_REL_PREFIX), + ) + ).order_by( + Build.branch + ).distinct() + + + return [ + dict( + name=branch[0] or "", + builders=[ + dict( + name=builder[0] or "", + branch=builder[1] or "", + slave=builder[2] or "", + builddir=builder[3] or "", + lastclobber=builder[4] or -1, + who=builder[5] or "", + ) + for builder in buildbot_branch_builders(db_session, branch[0]) + if all([builder[1], builder[2], builder[3]]) + ], + ) + for branch in branches + ] + + +def buildbot_branch_builders(db_session, branch): + """Return a dictionary of most recent ClobberTime grouped by + buildername. + """ + # Isolates the maximum lastclobber for each builddir on a branch + max_ct_sub_query = db_session.query( + sa.func.max(ClobberTime.lastclobber).label('lastclobber'), + ClobberTime.branch, + ClobberTime.builddir, + ).group_by( + ClobberTime.branch, + ClobberTime.builddir, + ).filter( + ClobberTime.branch == branch + ).subquery() + + # Finds the "greatest n per group" by joining with the + # max_ct_sub_query + # This is necessary to get the correct "who" values + sub_query = db_session.query( + ClobberTime + ).join( + max_ct_sub_query, + sa.and_( + ClobberTime.builddir == max_ct_sub_query.c.builddir, + ClobberTime.lastclobber == max_ct_sub_query.c.lastclobber, + ClobberTime.branch == max_ct_sub_query.c.branch, + ), + ).subquery() + + # Attaches builddir, along with their max lastclobber to a + # buildername + return db_session.query( + Build.buildername, + sub_query.c.branch, + sub_query.c.slave, + sub_query.c.builddir, + sub_query.c.lastclobber, + sub_query.c.who + ).outerjoin( + sub_query, + Build.builddir == sub_query.c.builddir, + ).filter( + Build.branch == branch, + sa.not_(Build.buildername.startswith(BUILDBOT_BUILDER_REL_PREFIX)) + ).order_by( + Build.buildername + ).distinct() + + +## TODO: this will change with tc authentication, it should be passed +#try: +# who = current_user.authenticated_email +#except AttributeError: +# if current_user.anonymous: +# who = 'anonymous' +# else: +# # TokenUser doesn't show up as anonymous; but also has no +# # authenticated_email +# who = 'automation' + +def buildbot_clobber(db_session, branch, slave, builddir, who, log=None): + """ TODO: + """ + + builder = ClobberTime.unique_hash(branch, slave, builddir) + + match = re.search('^' + BUILDBOT_BUILDDIR_REL_PREFIX + '.*', builddir) + if match is None: + + if log: + log.debug('Clobbering builder: {}'.format(builder)) + + clobberer_time = ClobberTime.as_unique( + db_session, + branch=branch, + slave=slave, + builddir=builddir, + ) + clobberer_time.lastclobber = int(time.time()) + clobberer_time.who = who + + db_session.add(clobberer_time) + db_session.commit() + + if log: + log.debug('Clobbered builder: {}'.format(builder)) + + return clobberer_time + + + if log: + log.debug('Skipping clobbering of builder: {}'.format(builder)) + + +def taskcluster_branches(): + """Dict of workerTypes per branch with their respected workerTypes + """ + index = tc.Index() + queue = tc.Queue() + + result = index.listNamespaces('gecko.v2', dict(limit=1000)) + + branches = { + i['name']: dict(name=i['name'], workerTypes=dict()) + for i in result.get('namespaces', []) + } + + for branchName, branch in branches.items(): + + # decision task might not exist + try: + decision_task = index.findTask( + TASKCLUSTER_DECISION_NAMESPACE % branchName) + decision_graph = queue.getLatestArtifact( + decision_task['taskId'], 'public/graph.json') + except tc.exceptions.TaskclusterRestFailure: + continue + + for task in decision_graph.get('tasks', []): + task = task['task'] + task_cache = task.get('payload', dict()).get('cache', dict()) + + provisionerId = task.get('provisionerId') + if provisionerId: + branch['provisionerId'] = provisionerId + + workerType = task.get('workerType') + if workerType: + branch['workerTypes'].setdefault( + workerType, dict(name=workerType, caches=[])) + + if len(task_cache) > 0: + branch['workerTypes'][workerType]['caches'] = list(set( + branch['workerTypes'][workerType]['caches'] + + task_cache.keys() + )) + + return branches diff --git a/src/relengapi_clobberer/relengapi_clobberer/swagger.yml b/src/relengapi_clobberer/relengapi_clobberer/swagger.yml index 3f1e3270..70fdd230 100644 --- a/src/relengapi_clobberer/relengapi_clobberer/swagger.yml +++ b/src/relengapi_clobberer/relengapi_clobberer/swagger.yml @@ -19,9 +19,28 @@ paths: post: operationId: "relengapi_clobberer.api.post_buildbot" + parameters: + - name: body + in: body + description: List of Builds to clobber. + required: true + schema: + type: array + items: + $ref: '#/definitions/Builder' responses: 200: - description: Branches clobbered + description: Builders clobbered + schema: + type: array + items: + $ref: '#/definitions/Success' + 500: + description: Something went wrong when clobbering builders + schema: + type: array + items: + $ref: '#/definitions/Error' /taskcluster: @@ -31,10 +50,6 @@ paths: responses: 200: description: An array of branches - schema: - type: array - items: - $ref: '#/definitions/Branch' post: operationId: "relengapi_clobberer.api.post_taskcluster" @@ -44,48 +59,52 @@ paths: definitions: - BuildTime: - type: object - description: Definition of a BuildTime - required: - - lastclobber - properties: - branch: - type: string - builddir: - type: string - lastclobber: - type: integer - who: - type: string + Success: + type: string - Builder: + Error: type: object - description: Definition of a WorkerType required: - - name - - build_times + - error_title properties: - name: + error_title: + type: string + error_message: type: string - description: Name of WorkerType - build_times: - type: array - description: Build times - items: - $ref: '#/definitions/BuildTime' Branch: type: object - description: Definition of a Branch required: - name - builders properties: name: type: string - description: Name of Branch builders: type: array + description: Builders per branch items: $ref: '#/definitions/Builder' + + Builder: + type: object + required: + - name + - branch + - builddir + - slave + - lastclobber + - who + properties: + name: + type: string + branch: + type: string + builddir: + type: string + slave: + type: string + lastclobber: + type: integer + who: + type: string diff --git a/src/relengapi_common/relengapi_common/__init__.py b/src/relengapi_common/relengapi_common/__init__.py index d0b3eff3..836033a8 100644 --- a/src/relengapi_common/relengapi_common/__init__.py +++ b/src/relengapi_common/relengapi_common/__init__.py @@ -39,15 +39,22 @@ def create_app(name, extensions=[], config=None, **kw): os.path.join(os.path.dirname(__file__), 'templates')) for extension in [log, auth, api, cache] + extensions: - extension_name = extension.__name__.split('.')[-1] - setattr(app, extension_name, extension.init_app(app)) + if type(extension) is tuple: + extension_name, extension_init = extension + elif not hasattr(extension, 'init_app'): + extension_name = None + extension_init = extension + else: + extension_name = extension.__name__.split('.')[-1] + extension_init = extension.init_app + + _app = extension_init(app) + if _app and extension_name is not None: + setattr(app, extension_name, _app) + if hasattr(app, 'log'): app.log.debug('extension `%s` configured.' % extension_name) - # configure/initialize specific app features - # aws -> tooltool, archiver (only s3 needed) - # memcached -> treestatus (via https://pythonhosted.org/Flask-Cache) - return app diff --git a/src/relengapi_common/relengapi_common/api.py b/src/relengapi_common/relengapi_common/api.py index 1816d9d1..8c079e93 100644 --- a/src/relengapi_common/relengapi_common/api.py +++ b/src/relengapi_common/relengapi_common/api.py @@ -4,7 +4,130 @@ from __future__ import absolute_import -from flask_restplus import Api +import connexion +import logging +import pathlib +import werkzeug.exceptions + + +logger = logging.getLogger('relengapi_common.api') + + +def common_error_handler(exception): + """ + TODO: add description + + :param extension: TODO + :type exception: Exception + + :rtype: TODO: + """ + + if not isinstance(exception, werkzeug.exceptions.HTTPException): + exception = werkzeug.exceptions.InternalServerError() + + return connexion.problem( + title=exception.name, + detail=exception.description, + status=exception.code, + ) + + +class Api: + """ + TODO: add description + TODO: annotate class + """ + + def __init__(self, app): + """ + TODO: add description + TODO: annotate function + """ + self.__app = app + + logger.debug('Setting JSON encoder.') + app.json_encoder = connexion.decorators.produces.JSONEncoder + + logger.debug('Setting common error handler for all error codes.') + for error_code in werkzeug.exceptions.default_exceptions: + app.register_error_handler(error_code, common_error_handler) + + def register(self, + swagger_file, + base_url=None, + arguments=None, + auth_all_paths=False, + swagger_json=True, + swagger_ui=True, + swagger_path=None, + swagger_url="swagger", + validate_responses=True, + strict_validation=True, + resolver=connexion.resolver.Resolver(), + ): + """ + Adds an API to the application based on a swagger file + + :param swagger_file: swagger file with the specification + :type swagger_file: str + + :param base_url: base path where to add this api + :type base_url: str | None + + :param arguments: api version specific arguments to replace on the + specification + :type arguments: dict | None + + :param auth_all_paths: whether to authenticate not defined paths + :type auth_all_paths: bool + + :param swagger_json: whether to include swagger json or not + :type swagger_json: bool + + :param swagger_ui: whether to include swagger ui or not + :type swagger_ui: bool + + :param swagger_path: path to swagger-ui directory + :type swagger_path: string | None + + :param swagger_url: URL to access swagger-ui documentation + :type swagger_url: string | None + + :param validate_responses: True enables validation. Validation errors + generate HTTP 500 responses. + :type validate_responses: bool + + :param strict_validation: True enables validation on invalid request + parameters + :type strict_validation: bool + + :param resolver: Operation resolver. + :type resolver: connexion.resolver.Resolver | types.FunctionType + + :rtype: None + """ + + if hasattr(resolver, '__call__'): + resolver = connexion.resolver.Resolver(resolver) + + logger.debug('Adding API: %s', swagger_file) + + self.__api = connexion.api.Api( + swagger_yaml_path=pathlib.Path(swagger_file), + base_url=base_url, + arguments=arguments, + swagger_json=swagger_json, + swagger_ui=swagger_ui, + swagger_path=swagger_path, + swagger_url=swagger_url, + resolver=resolver, + validate_responses=validate_responses, + strict_validation=strict_validation, + auth_all_paths=auth_all_paths, + debug=self.__app.debug, + ) + self.__app.register_blueprint(self.__api.blueprint) def init_app(app): diff --git a/src/relengapi_common/relengapi_common/cache.py b/src/relengapi_common/relengapi_common/cache.py index cddbd2a9..fe8c7670 100644 --- a/src/relengapi_common/relengapi_common/cache.py +++ b/src/relengapi_common/relengapi_common/cache.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from __future__ import absolute_import + from flask_cache import Cache diff --git a/src/relengapi_common/relengapi_common/db.py b/src/relengapi_common/relengapi_common/db.py index a42f612b..05473f42 100644 --- a/src/relengapi_common/relengapi_common/db.py +++ b/src/relengapi_common/relengapi_common/db.py @@ -8,22 +8,14 @@ from flask import g from flask_sqlalchemy import SQLAlchemy +db = SQLAlchemy() + def init_app(app): - db = SQLAlchemy() db.init_app(app) - - # ensure tables get created - # TODO: for dbname in db.database_names: - # TODO: app.log.info("creating tables for database %s", dbname) - - # TODO: meta = db.metadata[dbname] - # TODO: engine = db.engine(dbname) - # TODO: meta.create_all(bind=engine, checkfirst=True) + db.create_all(app=app) @app.before_request def setup_request(): g.db = app.db return db - - diff --git a/src/relengapi_common/requirements.txt b/src/relengapi_common/requirements.txt index 2cbb756a..ae183fe3 100644 --- a/src/relengapi_common/requirements.txt +++ b/src/relengapi_common/requirements.txt @@ -4,6 +4,6 @@ Flask-Cors Flask-Login Flask-SQLAlchemy Logbook -flask-restplus +connexion structlog taskcluster