diff --git a/nomad/admin/__init__.py b/nomad/admin/__init__.py index bc1477afadd3624e6fb1934279ec0f9f02dd93f5..2425b6f3082932780cdca55f1ed238f624f97aa8 100644 --- a/nomad/admin/__init__.py +++ b/nomad/admin/__init__.py @@ -19,8 +19,8 @@ Swagger/bravado based python client library for the API and various usefull shel from nomad.utils import POPO from . import upload, run -from .__main__ import cli as cli_main +from .cli import cli -def cli(): - cli_main(obj=POPO()) # pylint: disable=E1120,E1123 +def run_cli(): + cli(obj=POPO()) # pylint: disable=E1120,E1123 diff --git a/nomad/admin/__main__.py b/nomad/admin/__main__.py index 240b10cdd93641f13560a7f5558b7a1f8c249b6e..df3ff3a63be7fea727fcbfca9d144e9fd9135500 100644 --- a/nomad/admin/__main__.py +++ b/nomad/admin/__main__.py @@ -12,137 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import click -import logging -import os -import sys -import shutil -from tabulate import tabulate -from elasticsearch_dsl import A - -from nomad import config as nomad_config, infrastructure, processing -from nomad.search import Search - - -@click.group(help='''The nomad admin command to do nasty stuff directly on the databases. - Remember: With great power comes great responsibility!''') -@click.option('-v', '--verbose', help='sets log level to info', is_flag=True) -@click.option('--debug', help='sets log level to debug', is_flag=True) -@click.option('--config', help='the config file to use') -@click.pass_context -def cli(ctx, verbose: bool, debug: bool, config: str): - if config is not None: - nomad_config.load_config(config_file=config) - - if debug: - nomad_config.console_log_level = logging.DEBUG - elif verbose: - nomad_config.console_log_level = logging.INFO - else: - nomad_config.console_log_level = logging.WARNING - - nomad_config.service = os.environ.get('NOMAD_SERVICE', 'admin') - infrastructure.setup_logging() - - -@cli.command(help='Runs tests and linting. Useful before commit code.') -@click.option('--skip-tests', help='Do not test, just do code checks.', is_flag=True) -def qa(skip_tests: bool): - os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) - ret_code = 0 - if not skip_tests: - click.echo('Run tests ...') - ret_code += os.system('python -m pytest -svx tests') - click.echo('Run code style checks ...') - ret_code += os.system('python -m pycodestyle --ignore=E501,E701 nomad tests') - click.echo('Run linter ...') - ret_code += os.system('python -m pylint --load-plugins=pylint_mongoengine nomad tests') - click.echo('Run static type checks ...') - ret_code += os.system('python -m mypy --ignore-missing-imports --follow-imports=silent --no-strict-optional nomad tests') - - sys.exit(ret_code) - - -@cli.command(help='Checks consistency of files and es vs mongo and deletes orphan entries.') -@click.option('--dry', is_flag=True, help='Do not delete anything, just check.') -@click.option('--skip-calcs', is_flag=True, help='Skip cleaning calcs with missing uploads.') -@click.option('--skip-fs', is_flag=True, help='Skip cleaning the filesystem.') -@click.option('--skip-es', is_flag=True, help='Skip cleaning the es index.') -def clean(dry, skip_calcs, skip_fs, skip_es): - infrastructure.setup_logging() - mongo_client = infrastructure.setup_mongo() - infrastructure.setup_elastic() - - if not skip_calcs: - uploads_for_calcs = mongo_client[nomad_config.mongo.db_name]['calc'].distinct('upload_id') - uploads = {} - for upload in mongo_client[nomad_config.mongo.db_name]['upload'].distinct('_id'): - uploads[upload] = True - - missing_uploads = [] - for upload_for_calc in uploads_for_calcs: - if upload_for_calc not in uploads: - missing_uploads.append(upload_for_calc) - - if not dry and len(missing_uploads) > 0: - input('Will delete calcs (mongo + es) for %d missing uploads. Press any key to continue ...' % len(missing_uploads)) - - for upload in missing_uploads: - mongo_client[nomad_config.mongo.db_name]['calc'].remove(dict(upload_id=upload)) - Search(index=nomad_config.elastic.index_name).query('term', upload_id=upload).delete() - else: - print('Found %s uploads that have calcs in mongo, but there is no upload entry.' % len(missing_uploads)) - print('List first 10:') - for upload in missing_uploads[:10]: - print(upload) - - if not skip_fs: - upload_dirs = [] - for bucket in [nomad_config.fs.public, nomad_config.fs.staging]: - for prefix in os.listdir(bucket): - for upload in os.listdir(os.path.join(bucket, prefix)): - upload_dirs.append((upload, os.path.join(bucket, prefix, upload))) - - to_delete = list( - path for upload, path in upload_dirs - if processing.Upload.objects(upload_id=upload).first() is None) - - if not dry and len(to_delete) > 0: - input('Will delete %d upload directories. Press any key to continue ...' % len(to_delete)) - - for path in to_delete: - shutil.rmtree(path) - else: - print('Found %d upload directories with no upload in mongo.' % len(to_delete)) - print('List first 10:') - for path in to_delete[:10]: - print(path) - - if not skip_es: - search = Search(index=nomad_config.elastic.index_name) - search.aggs.bucket('uploads', A('terms', field='upload_id', size=12000)) - response = search.execute() - - to_delete = list( - (bucket.key, bucket.doc_count) - for bucket in response.aggregations.uploads.buckets - if processing.Upload.objects(upload_id=bucket.key).first() is None) - - calcs = 0 - for _, upload_calcs in to_delete: - calcs += upload_calcs - - if not dry and len(to_delete) > 0: - input( - 'Will delete %d calcs in %d uploads from ES. Press any key to continue ...' % - (calcs, len(to_delete))) - for upload, _ in to_delete: - Search(index=nomad_config.elastic.index_name).query('term', upload_id=upload).delete() - else: - print('Found %d calcs in %d uploads from ES with no upload in mongo.' % (calcs, len(to_delete))) - print('List first 10:') - tabulate(to_delete, headers=['id', '#calcs']) +from nomad.utils import POPO +from .cli import cli if __name__ == '__main__': - cli(obj={}) # pylint: disable=E1120,E1123 + print('#######################') + cli(obj=POPO()) # pylint: disable=E1120,E1123 diff --git a/nomad/admin/cli.py b/nomad/admin/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..29b312d0123113e3f4153545026d497576e1ab48 --- /dev/null +++ b/nomad/admin/cli.py @@ -0,0 +1,144 @@ +# Copyright 2018 Markus Scheidgen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an"AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import click +import logging +import os +import sys +import shutil +from tabulate import tabulate +from elasticsearch_dsl import A + +from nomad import config as nomad_config, infrastructure, processing +from nomad.search import Search + + +@click.group(help='''The nomad admin command to do nasty stuff directly on the databases. + Remember: With great power comes great responsibility!''') +@click.option('-v', '--verbose', help='sets log level to info', is_flag=True) +@click.option('--debug', help='sets log level to debug', is_flag=True) +@click.option('--config', help='the config file to use') +@click.pass_context +def cli(ctx, verbose: bool, debug: bool, config: str): + if config is not None: + nomad_config.load_config(config_file=config) + + if debug: + nomad_config.console_log_level = logging.DEBUG + elif verbose: + nomad_config.console_log_level = logging.INFO + else: + nomad_config.console_log_level = logging.WARNING + + nomad_config.service = os.environ.get('NOMAD_SERVICE', 'admin') + infrastructure.setup_logging() + + +@cli.command(help='Runs tests and linting. Useful before commit code.') +@click.option('--skip-tests', help='Do not test, just do code checks.', is_flag=True) +def qa(skip_tests: bool): + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + ret_code = 0 + if not skip_tests: + click.echo('Run tests ...') + ret_code += os.system('python -m pytest -svx tests') + click.echo('Run code style checks ...') + ret_code += os.system('python -m pycodestyle --ignore=E501,E701 nomad tests') + click.echo('Run linter ...') + ret_code += os.system('python -m pylint --load-plugins=pylint_mongoengine nomad tests') + click.echo('Run static type checks ...') + ret_code += os.system('python -m mypy --ignore-missing-imports --follow-imports=silent --no-strict-optional nomad tests') + + sys.exit(ret_code) + + +@cli.command(help='Checks consistency of files and es vs mongo and deletes orphan entries.') +@click.option('--dry', is_flag=True, help='Do not delete anything, just check.') +@click.option('--skip-calcs', is_flag=True, help='Skip cleaning calcs with missing uploads.') +@click.option('--skip-fs', is_flag=True, help='Skip cleaning the filesystem.') +@click.option('--skip-es', is_flag=True, help='Skip cleaning the es index.') +def clean(dry, skip_calcs, skip_fs, skip_es): + infrastructure.setup_logging() + mongo_client = infrastructure.setup_mongo() + infrastructure.setup_elastic() + + if not skip_calcs: + uploads_for_calcs = mongo_client[nomad_config.mongo.db_name]['calc'].distinct('upload_id') + uploads = {} + for upload in mongo_client[nomad_config.mongo.db_name]['upload'].distinct('_id'): + uploads[upload] = True + + missing_uploads = [] + for upload_for_calc in uploads_for_calcs: + if upload_for_calc not in uploads: + missing_uploads.append(upload_for_calc) + + if not dry and len(missing_uploads) > 0: + input('Will delete calcs (mongo + es) for %d missing uploads. Press any key to continue ...' % len(missing_uploads)) + + for upload in missing_uploads: + mongo_client[nomad_config.mongo.db_name]['calc'].remove(dict(upload_id=upload)) + Search(index=nomad_config.elastic.index_name).query('term', upload_id=upload).delete() + else: + print('Found %s uploads that have calcs in mongo, but there is no upload entry.' % len(missing_uploads)) + print('List first 10:') + for upload in missing_uploads[:10]: + print(upload) + + if not skip_fs: + upload_dirs = [] + for bucket in [nomad_config.fs.public, nomad_config.fs.staging]: + for prefix in os.listdir(bucket): + for upload in os.listdir(os.path.join(bucket, prefix)): + upload_dirs.append((upload, os.path.join(bucket, prefix, upload))) + + to_delete = list( + path for upload, path in upload_dirs + if processing.Upload.objects(upload_id=upload).first() is None) + + if not dry and len(to_delete) > 0: + input('Will delete %d upload directories. Press any key to continue ...' % len(to_delete)) + + for path in to_delete: + shutil.rmtree(path) + else: + print('Found %d upload directories with no upload in mongo.' % len(to_delete)) + print('List first 10:') + for path in to_delete[:10]: + print(path) + + if not skip_es: + search = Search(index=nomad_config.elastic.index_name) + search.aggs.bucket('uploads', A('terms', field='upload_id', size=12000)) + response = search.execute() + + to_delete = list( + (bucket.key, bucket.doc_count) + for bucket in response.aggregations.uploads.buckets + if processing.Upload.objects(upload_id=bucket.key).first() is None) + + calcs = 0 + for _, upload_calcs in to_delete: + calcs += upload_calcs + + if not dry and len(to_delete) > 0: + input( + 'Will delete %d calcs in %d uploads from ES. Press any key to continue ...' % + (calcs, len(to_delete))) + for upload, _ in to_delete: + Search(index=nomad_config.elastic.index_name).query('term', upload_id=upload).delete() + else: + print('Found %d calcs in %d uploads from ES with no upload in mongo.' % (calcs, len(to_delete))) + print('List first 10:') + tabulate(to_delete, headers=['id', '#calcs']) diff --git a/nomad/admin/run.py b/nomad/admin/run.py index 431f290da1e01604fee0be92554d65294e9e2ad9..ef9e7d0a4bf26089d14eb213915f7eda6d918624 100644 --- a/nomad/admin/run.py +++ b/nomad/admin/run.py @@ -17,7 +17,7 @@ import asyncio from concurrent.futures import ProcessPoolExecutor from nomad import config -from nomad.admin.__main__ import cli +from .cli import cli @cli.group(help='Run a nomad service locally (outside docker).') diff --git a/nomad/admin/upload.py b/nomad/admin/upload.py index 637f7166acf00b2dbd5592a7a829f33d83db0951..aa87a7ab0585f5ac38f0d2c3b293fa3315bd6792 100644 --- a/nomad/admin/upload.py +++ b/nomad/admin/upload.py @@ -18,7 +18,7 @@ from mongoengine import Q from pymongo import UpdateOne from nomad import processing as proc, config, infrastructure, utils, search, files, coe_repo -from .__main__ import cli +from .cli import cli @cli.group(help='Upload related commands') @@ -42,7 +42,7 @@ def upload(ctx, user: str, staging: bool, processing: bool, outdated: bool): if outdated: uploads = proc.Calc._get_collection().distinct( 'upload_id', - {'metadata.nomad_version': { '$ne': config.version}}) + {'metadata.nomad_version': {'$ne': config.version}}) query &= Q(upload_id__in=uploads) ctx.obj.query = query @@ -156,13 +156,13 @@ def re_process(ctx, uploads): logger = utils.get_logger(__name__) print('%d uploads selected, re-processing ...' % uploads.count()) - def re_process_upload(upload: str): + def re_process_upload(upload): logger.info('re-processing started', upload_id=upload.upload_id) upload.re_process_upload() upload.block_until_complete(interval=.1) - logger.info('re-processing complete', upload_id=upload_id) + logger.info('re-processing complete', upload_id=upload.upload_id) count = 0 for upload in uploads: diff --git a/nomad/api/upload.py b/nomad/api/upload.py index 0cd90684613ac939f098618922a9b545372a08ca..225cdd3a873869734f96b493c31a7f1e41ab5a49 100644 --- a/nomad/api/upload.py +++ b/nomad/api/upload.py @@ -445,7 +445,7 @@ class UploadResource(Resource): @login_really_required def post(self, upload_id): """ - Execute an upload operation. Available operations: ``publish`` + Execute an upload operation. Available operations are ``publish`` and ``re-process`` Publish accepts further meta data that allows to provide coauthors, comments, external references, etc. See the model for details. The fields that start with @@ -453,6 +453,10 @@ class UploadResource(Resource): Publish changes the visibility of the upload. Clients can specify the visibility via meta data. + + Re-process will re-process the upload and produce updated repository metadata and + archive. Only published uploads that are not processing at the moment are allowed. + Only for uploads where calculations have been processed with an older nomad version. """ try: upload = Upload.get(upload_id) @@ -489,8 +493,21 @@ class UploadResource(Resource): abort(400, message='The upload is still/already processed') return upload, 200 + elif operation == 're-process': + if upload.tasks_running or not upload.published: + abort(400, message='Can only non processing, re-process published uploads') + + if len(metadata) > 0: + abort(400, message='You can not provide metadata for re-processing') + + if len(upload.outdated_calcs) == 0: + abort(400, message='You can only re-process uploads with at least one outdated calculation') + + upload.re_process_upload() + + return upload, 200 - abort(400, message='Unsuported operation %s.' % operation) + abort(400, message='Unsupported operation %s.' % operation) upload_command_model = api.model('UploadCommand', { diff --git a/nomad/client/__init__.py b/nomad/client/__init__.py index 12266f4124735bb7aa25f664c8a8eb99d9a83fe4..7963bb0008b740e9a6cc6ed7370fafe6665ed425 100644 --- a/nomad/client/__init__.py +++ b/nomad/client/__init__.py @@ -17,5 +17,9 @@ Swagger/bravado based python client library for the API and various usefull shel """ from . import local, migration, upload, integrationtests, parse -from .__main__ import cli, create_client +from .main import cli, create_client from .upload import stream_upload_with_client + + +def run_cli(): + cli() # pylint: disable=E1120 diff --git a/nomad/client/__main__.py b/nomad/client/__main__.py index 4d137e784151e9e721f70f0348086b76fcf1b1ab..3ea324acd171167e1a7cb9d1f33aee9f8214a6e2 100644 --- a/nomad/client/__main__.py +++ b/nomad/client/__main__.py @@ -12,106 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys -import requests -import click -import logging -from bravado.requests_client import RequestsClient -from bravado.client import SwaggerClient -from urllib.parse import urlparse - -import nomad.client -from nomad import config as nomad_config -from nomad import utils, infrastructure - - -def create_client(): - return _create_client() - - -def _create_client(*args, **kwargs): - return __create_client(*args, **kwargs) - - -def __create_client(user: str = nomad_config.client.user, password: str = nomad_config.client.password, ssl_verify: bool = True): - """ A factory method to create the client. """ - host = urlparse(nomad_config.client.url).netloc.split(':')[0] - - if not ssl_verify: - import warnings - warnings.filterwarnings("ignore") - - http_client = RequestsClient(ssl_verify=ssl_verify) - if user is not None: - http_client.set_basic_auth(host, user, password) - - client = SwaggerClient.from_url( - '%s/swagger.json' % nomad_config.client.url, - http_client=http_client) - - utils.get_logger(__name__).info('created bravado client', user=user) - - return client - - -def handle_common_errors(func): - def wrapper(*args, **kwargs): - try: - func(*args, **kwargs) - except requests.exceptions.ConnectionError: - click.echo( - '\nCould not connect to nomad at %s. ' - 'Check connection and url.' % nomad_config.client.url) - sys.exit(0) - return wrapper - - -@click.group() -@click.option('-n', '--url', default=nomad_config.client.url, help='The URL where nomad is running, default is "%s".' % nomad_config.client.url) -@click.option('-u', '--user', default=None, help='the user name to login, default is "%s" login.' % nomad_config.client.user) -@click.option('-w', '--password', default=nomad_config.client.password, help='the password used to login.') -@click.option('-v', '--verbose', help='sets log level to info', is_flag=True) -@click.option('--no-ssl-verify', help='disables SSL verificaton when talking to nomad.', is_flag=True) -@click.option('--debug', help='sets log level to debug', is_flag=True) -@click.option('--config', help='the config file to use') -def cli(url: str, verbose: bool, debug: bool, user: str, password: str, config: str, no_ssl_verify: bool): - if config is not None: - nomad_config.load_config(config_file=config) - - if debug: - nomad_config.console_log_level = logging.DEBUG - elif verbose: - nomad_config.console_log_level = logging.INFO - else: - nomad_config.console_log_level = logging.WARNING - - nomad_config.service = os.environ.get('NOMAD_SERVICE', 'client') - infrastructure.setup_logging() - - logger = utils.get_logger(__name__) - - logger.info('Used nomad is %s' % url) - logger.info('Used user is %s' % user) - - nomad_config.client.url = url - - global _create_client - - def _create_client(*args, **kwargs): # pylint: disable=W0612 - if user is not None: - logger.info('create client', user=user) - return __create_client(user=user, password=password, ssl_verify=not no_ssl_verify) - else: - logger.info('create anonymous client') - return __create_client(ssl_verify=not no_ssl_verify) - - -@cli.command(help='Attempts to reset the nomad.') -def reset(): - from .__main__ import create_client - create_client().admin.exec_reset_command().response() - +from .main import cli if __name__ == '__main__': - nomad.client.cli() # pylint: disable=E1120 + cli() # pylint: disable=E1120 diff --git a/nomad/client/integrationtests.py b/nomad/client/integrationtests.py index e330009f59461b6f71b0441f9e35b67365f413a5..e0018f4fbb9d8041b5cadc974b9547ef2ecf39fb 100644 --- a/nomad/client/integrationtests.py +++ b/nomad/client/integrationtests.py @@ -19,7 +19,7 @@ as a final integration test. import time -from .__main__ import cli +from .main import cli example_file = 'tests/data/proc/examples_vasp.zip' @@ -27,7 +27,7 @@ example_file = 'tests/data/proc/examples_vasp.zip' @cli.command(help='Runs a few example operations as a test.') def integrationtests(): - from .__main__ import create_client + from .main import create_client client = create_client() print('upload with multiple code data') diff --git a/nomad/client/local.py b/nomad/client/local.py index e6982bb427cd522c514101fc294d3c0b1c3bd439..3c99d6f85ac30c85efbe263e038024e6b0330191 100644 --- a/nomad/client/local.py +++ b/nomad/client/local.py @@ -28,7 +28,7 @@ from nomad.datamodel import CalcWithMetadata from nomad.parsing import LocalBackend from nomad.client.parse import parse, normalize, normalize_all -from .__main__ import cli +from .main import cli class CalcProcReproduction: @@ -58,7 +58,7 @@ class CalcProcReproduction: self.mainfile = mainfile self.parser = None - from .__main__ import create_client + from .main import create_client client = create_client() if self.mainfile is None: try: diff --git a/nomad/client/main.py b/nomad/client/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b84087fdc2b171e629fc9e1406df130f7bca1b5c --- /dev/null +++ b/nomad/client/main.py @@ -0,0 +1,112 @@ +# Copyright 2018 Markus Scheidgen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an"AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import requests +import click +import logging +from bravado.requests_client import RequestsClient +from bravado.client import SwaggerClient +from urllib.parse import urlparse + +from nomad import config as nomad_config +from nomad import utils, infrastructure + + +def create_client(): + return _create_client() + + +def _create_client(*args, **kwargs): + return __create_client(*args, **kwargs) + + +def __create_client(user: str = nomad_config.client.user, password: str = nomad_config.client.password, ssl_verify: bool = True): + """ A factory method to create the client. """ + host = urlparse(nomad_config.client.url).netloc.split(':')[0] + + if not ssl_verify: + import warnings + warnings.filterwarnings("ignore") + + http_client = RequestsClient(ssl_verify=ssl_verify) + if user is not None: + http_client.set_basic_auth(host, user, password) + + client = SwaggerClient.from_url( + '%s/swagger.json' % nomad_config.client.url, + http_client=http_client) + + utils.get_logger(__name__).info('created bravado client', user=user) + + return client + + +def handle_common_errors(func): + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except requests.exceptions.ConnectionError: + click.echo( + '\nCould not connect to nomad at %s. ' + 'Check connection and url.' % nomad_config.client.url) + sys.exit(0) + return wrapper + + +@click.group() +@click.option('-n', '--url', default=nomad_config.client.url, help='The URL where nomad is running, default is "%s".' % nomad_config.client.url) +@click.option('-u', '--user', default=None, help='the user name to login, default is "%s" login.' % nomad_config.client.user) +@click.option('-w', '--password', default=nomad_config.client.password, help='the password used to login.') +@click.option('-v', '--verbose', help='sets log level to info', is_flag=True) +@click.option('--no-ssl-verify', help='disables SSL verificaton when talking to nomad.', is_flag=True) +@click.option('--debug', help='sets log level to debug', is_flag=True) +@click.option('--config', help='the config file to use') +def cli(url: str, verbose: bool, debug: bool, user: str, password: str, config: str, no_ssl_verify: bool): + if config is not None: + nomad_config.load_config(config_file=config) + + if debug: + nomad_config.console_log_level = logging.DEBUG + elif verbose: + nomad_config.console_log_level = logging.INFO + else: + nomad_config.console_log_level = logging.WARNING + + nomad_config.service = os.environ.get('NOMAD_SERVICE', 'client') + infrastructure.setup_logging() + + logger = utils.get_logger(__name__) + + logger.info('Used nomad is %s' % url) + logger.info('Used user is %s' % user) + + nomad_config.client.url = url + + global _create_client + + def _create_client(*args, **kwargs): # pylint: disable=W0612 + if user is not None: + logger.info('create client', user=user) + return __create_client(user=user, password=password, ssl_verify=not no_ssl_verify) + else: + logger.info('create anonymous client') + return __create_client(ssl_verify=not no_ssl_verify) + + +@cli.command(help='Attempts to reset the nomad.') +def reset(): + from .main import create_client + create_client().admin.exec_reset_command().response() diff --git a/nomad/client/migration.py b/nomad/client/migration.py index 79dac17aa60e1afa2478fc6e231a2a55cc72bffa..6653117a613e04c7560e075b7d5848a3d1c326c7 100644 --- a/nomad/client/migration.py +++ b/nomad/client/migration.py @@ -26,7 +26,7 @@ import json from nomad import config, infrastructure from nomad.migration import NomadCOEMigration, SourceCalc, Package, missing_calcs_data -from .__main__ import cli +from .main import cli def _Migration(**kwargs) -> NomadCOEMigration: diff --git a/nomad/client/parse.py b/nomad/client/parse.py index e917a10ef4f15090ba0de9d223516928a88081d9..395cbb1117edd30857fc9aec038d513f8b3b5df2 100644 --- a/nomad/client/parse.py +++ b/nomad/client/parse.py @@ -9,7 +9,7 @@ from nomad.parsing import LocalBackend, parser_dict, match_parser from nomad.normalizing import normalizers from nomad.datamodel import CalcWithMetadata -from .__main__ import cli +from .main import cli def parse( diff --git a/nomad/client/upload.py b/nomad/client/upload.py index b40fd209cc481f20aec852a8d33e675079806d72..1b86a37fd846e2a1be22d5ae16a364e80013e8ad 100644 --- a/nomad/client/upload.py +++ b/nomad/client/upload.py @@ -22,7 +22,7 @@ import requests from nomad import utils, config from nomad.processing import FAILURE, SUCCESS -from .__main__ import cli, create_client +from .main import cli, create_client def stream_upload_with_client(client, stream, name=None): diff --git a/nomad/config.py b/nomad/config.py index ac13f62664972494645952b0f699dff50e023432..b4f6bfe44ad7d1152f571785a9f640927ccdb73d 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -259,7 +259,7 @@ def load_config(config_file: str = os.environ.get('NOMAD_CONFIG', 'nomad.yaml')) if os.path.exists(config_file): with open(config_file, 'r') as stream: try: - config_data = yaml.load(stream) + config_data = yaml.load(stream, Loader=getattr(yaml, 'FullLoader')) except yaml.YAMLError as e: logger.error('cannot read nomad config', exc_info=e) diff --git a/nomad/processing/data.py b/nomad/processing/data.py index c0917368c6ca140598b7e4c46cb9e2b70913d366..d4089a035bf39acc943ad2d91f244dac5b420b15 100644 --- a/nomad/processing/data.py +++ b/nomad/processing/data.py @@ -613,6 +613,9 @@ class Upload(Proc): """ assert self.published + logger = self.get_logger() + logger.info('started to re-process') + self.reset() # mock the steps of actual processing self._continue_with('uploading') @@ -861,6 +864,12 @@ class Upload(Proc): query = Calc.objects(upload_id=self.upload_id)[start:end] return query.order_by(order_by) if order_by is not None else query + @property + def outdated_calcs(self): + return Calc.objects( + upload_id=self.upload_id, tasks_status=SUCCESS, + metadata__nomad_version__ne=config.version) + @property def calcs(self): return Calc.objects(upload_id=self.upload_id, tasks_status=SUCCESS) diff --git a/setup.py b/setup.py index e4a77ef976666dcc5fcab443d84ae285d2fc978e..ae2b9b2e49dd86bedac0bf2259bde00fc0bd122d 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,6 @@ setup( install_requires=reqs, entry_points=''' [console_scripts] - nomad=nomad.client:cli - admin=nomad.admin:cli + nomad=nomad.client:run_cli + admin=nomad.admin:run_cli ''') diff --git a/tests/conftest.py b/tests/conftest.py index d33f597478ba4ebaee88e09fbd8087e09ff340f2..4a9d754f18f755cd307aeeb917b511b2decd5f36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -546,6 +546,22 @@ def non_empty_processed(non_empty_uploaded: Tuple[str, str], test_user: coe_repo return test_processing.run_processing(non_empty_uploaded, test_user) +@pytest.mark.timeout(config.tests.default_timeout) +@pytest.fixture(scope='function') +def published(non_empty_processed: processing.Upload, example_user_metadata) -> processing.Upload: + """ + Provides a processed upload. Upload was uploaded with test_user. + """ + non_empty_processed.compress_and_set_metadata(example_user_metadata) + non_empty_processed.publish_upload() + try: + non_empty_processed.block_until_complete(interval=.01) + except Exception: + pass + + return non_empty_processed + + @pytest.fixture(scope='function', params=[None, 'fairdi', 'coe']) def with_publish_to_coe_repo(monkeypatch, request): mode = request.param diff --git a/tests/test_api.py b/tests/test_api.py index 5932f88ea150957560b3cb88ff0a26da53e6b557..489b1749198d0cefc565c3917285233d919ad4b2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -447,6 +447,20 @@ class TestUploads: self.assert_published(client, admin_user_auth, upload['upload_id'], proc_infra, metadata) self.assert_published(client, admin_user_auth, upload['upload_id'], proc_infra, metadata, publish_with_metadata=False) + def test_post_re_process(self, client, published, test_user_auth, monkeypatch): + monkeypatch.setattr('nomad.config.version', 're_process_test_version') + monkeypatch.setattr('nomad.config.commit', 're_process_test_commit') + + upload_id = published.upload_id + rv = client.post( + '/uploads/%s' % upload_id, + headers=test_user_auth, + data=json.dumps(dict(operation='re-process')), + content_type='application/json') + + assert rv.status_code == 200 + assert self.block_until_completed(client, upload_id, test_user_auth) is not None + # TODO validate metadata (or all input models in API for that matter) # def test_post_bad_metadata(self, client, proc_infra, test_user_auth, postgres): # rv = client.put('/uploads/?local_path=%s' % example_file, headers=test_user_auth)