diff --git a/gui/src/components/App.js b/gui/src/components/App.js index f234f4bf00e74b0484da435357a58d66b903951d..1b37809e508819a78b83bb499506e38e31d600cf 100644 --- a/gui/src/components/App.js +++ b/gui/src/components/App.js @@ -198,8 +198,14 @@ class NavigationUnstyled extends React.Component { } componentDidMount() { - fetch(`${guiBase}/meta.json`) - .then((response) => response.json()) + fetch(`${guiBase}/meta.json`, { + method: 'GET', + cache: 'no-cache', + headers: { + 'Pragma': 'no-cache', + 'Cache-Control': 'no-cache, no-store' + } + }).then((response) => response.json()) .then((meta) => { if (meta.version !== packageJson.version) { console.log('GUI API version mismatch') diff --git a/nomad/app/api/raw.py b/nomad/app/api/raw.py index 9736303b14ed2f02f29f455dc3ade49470f92ef4..2afcf637ee0720aac16665148c540918aab8460b 100644 --- a/nomad/app/api/raw.py +++ b/nomad/app/api/raw.py @@ -366,7 +366,7 @@ class RawFileQueryResource(Resource): abort(400, message='bad parameter types') search_request = search.SearchRequest() - add_query(search_request) + add_query(search_request, search_request_parser) calcs = sorted([ (entry['upload_id'], entry['mainfile']) diff --git a/nomad/app/api/repo.py b/nomad/app/api/repo.py index 95a64bb35e7ce1483d963ed5b253b2ab412196e4..b0a148179e65e2b476f55db7e86605638f57e858 100644 --- a/nomad/app/api/repo.py +++ b/nomad/app/api/repo.py @@ -102,7 +102,7 @@ def add_common_parameters(request_parser): for quantity in search.quantities.values(): request_parser.add_argument( quantity.name, help=quantity.description, - action='append' if quantity.multi else None) + action=quantity.argparse_action if quantity.multi else None) repo_request_parser = pagination_request_parser.copy() @@ -129,14 +129,16 @@ search_request_parser = api.parser() add_common_parameters(search_request_parser) -def add_query(search_request: search.SearchRequest): +def add_query(search_request: search.SearchRequest, parser=repo_request_parser): """ Help that adds query relevant request parameters to the given SearchRequest. """ + args = {key: value for key, value in parser.parse_args().items() if value is not None} + # owner try: search_request.owner( - request.args.get('owner', 'all'), + args.get('owner', 'all'), g.user.user_id if g.user is not None else None) except ValueError as e: abort(401, getattr(e, 'message', 'Invalid owner parameter')) @@ -144,8 +146,8 @@ def add_query(search_request: search.SearchRequest): abort(400, getattr(e, 'message', 'Invalid owner parameter')) # time range - from_time_str = request.args.get('from_time', None) - until_time_str = request.args.get('until_time', None) + from_time_str = args.get('from_time', None) + until_time_str = args.get('until_time', None) try: from_time = rfc3339DateTime.parse(from_time_str) if from_time_str is not None else None @@ -156,7 +158,7 @@ def add_query(search_request: search.SearchRequest): # optimade try: - optimade = request.args.get('optimade', None) + optimade = args.get('optimade', None) if optimade is not None: q = filterparser.parse_filter(optimade) search_request.query(q) @@ -165,8 +167,7 @@ def add_query(search_request: search.SearchRequest): # search parameter search_request.search_parameters(**{ - key: request.args.getlist(key) if search.quantities[key] else request.args.get(key) - for key in request.args.keys() + key: value for key, value in args.items() if key not in ['optimade'] and key in search.quantities}) @@ -210,7 +211,7 @@ class RepoCalcsResource(Resource): """ search_request = search.SearchRequest() - add_query(search_request) + add_query(search_request, repo_request_parser) try: scroll = bool(request.args.get('scroll', False)) @@ -325,7 +326,7 @@ class RepoQuantityResource(Resource): """ search_request = search.SearchRequest() - add_query(search_request) + add_query(search_request, repo_quantity_search_request_parser) try: after = request.args.get('after', None) diff --git a/nomad/app/api/upload.py b/nomad/app/api/upload.py index 649e4517ccb6e9c68fb2caf986a117f01769b89a..2e2f8f687483286fbc1678266bbff8c9bfbba076 100644 --- a/nomad/app/api/upload.py +++ b/nomad/app/api/upload.py @@ -222,6 +222,7 @@ class UploadListResource(Resource): pagination=dict(total=total, page=page, per_page=per_page), results=results), 200 + @api.doc(security=list(api.authorizations.keys())) # weird bug, this should not be necessary @api.doc('upload') @api.expect(upload_metadata_parser) @api.response(400, 'To many uploads') diff --git a/nomad/config.py b/nomad/config.py index 35af6acc8475ff3e3e6b9674d1784cfbefe38891..3ee162f5a2e421a013eaa6db1265082925d0ad7e 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -187,7 +187,7 @@ client = NomadConfig( url='http://localhost:8000/fairdi/nomad/latest/api' ) -version = '0.6.0' +version = '0.7.0' commit = gitinfo.commit release = 'devel' domain = 'DFT' diff --git a/nomad/datamodel/base.py b/nomad/datamodel/base.py index b2f602c3967256fc4c4cda331a4f99c13089f084..1cd190c2b0822edd542c0f475b385fccb478bc19 100644 --- a/nomad/datamodel/base.py +++ b/nomad/datamodel/base.py @@ -260,6 +260,7 @@ class DomainQuantity: elastic_field: An optional elasticsearch key. Default is the name of the quantity. elastic_value: A collable that takes a :class:`CalcWithMetadata` as input and produces the value for the elastic search index. + argparse_action: Action to use on argparse, either append or split for multi values. Append is default. """ def __init__( @@ -268,7 +269,8 @@ class DomainQuantity: zero_aggs: bool = True, metadata_field: str = None, elastic_mapping: type = None, elastic_search_type: str = 'term', elastic_field: str = None, - elastic_value: Callable[[Any], Any] = None): + elastic_value: Callable[[Any], Any] = None, + argparse_action: str = 'append'): self._name: str = None self.description = description @@ -281,6 +283,7 @@ class DomainQuantity: self.elastic_search_type = elastic_search_type self.metadata_field = metadata_field self.elastic_field = elastic_field + self.argparse_action = argparse_action self.elastic_value = elastic_value if self.elastic_value is None: @@ -353,7 +356,9 @@ class Domain: pid=DomainQuantity(description='Search for the pid.'), raw_id=DomainQuantity(description='Search for the raw_id.'), mainfile=DomainQuantity(description='Search for the mainfile.'), - external_id=DomainQuantity(description='External user provided id. Does not have to be unique necessarily.'), + external_id=DomainQuantity( + description='External user provided id. Does not have to be unique necessarily.', + multi=True, argparse_action='split', elastic_search_type='terms'), dataset=DomainQuantity( elastic_field='datasets.name', multi=True, elastic_search_type='match', description='Search for a particular dataset by name.'), diff --git a/nomad/parsing/__init__.py b/nomad/parsing/__init__.py index 4375ac1aa55542e1b1497491ceb4f64abc844bb2..0481965589583002be8ac4c0b959361357b4ac5a 100644 --- a/nomad/parsing/__init__.py +++ b/nomad/parsing/__init__.py @@ -71,10 +71,11 @@ based on NOMAD-coe's *python-common* module. :members: """ -from typing import Callable, IO, Union +from typing import Callable, IO, Union, Dict import magic import gzip import bz2 +import lzma import os.path from nomad import files, config @@ -87,7 +88,8 @@ from nomad.parsing.artificial import TemplateParser, GenerateRandomParser, Chaos _compressions = { b'\x1f\x8b\x08': ('gz', gzip.open), - b'\x42\x5a\x68': ('bz2', bz2.open) + b'\x42\x5a\x68': ('bz2', bz2.open), + b'\xfd\x37\x7a': ('xz', lzma.open) } @@ -116,7 +118,7 @@ def match_parser(mainfile: str, upload_files: Union[str, files.StagingUploadFile with open(mainfile_path, 'rb') as f: compression, open_compressed = _compressions.get(f.read(3), (None, open)) - with open_compressed(mainfile_path, 'rb') as cf: + with open_compressed(mainfile_path, 'rb') as cf: # type: ignore buffer = cf.read(config.parser_matching_size) mime_type = magic.from_buffer(buffer, mime=True) @@ -147,14 +149,14 @@ parsers = [ LegacyParser( name='parsers/vasp', code_name='VASP', parser_class_name='vaspparser.VASPRunParserInterface', - mainfile_mime_re=r'(application/xml)|(text/.*)', + mainfile_mime_re=r'(application/.*)|(text/.*)', mainfile_contents_re=( r'^\s*<\?xml version="1\.0" encoding="ISO-8859-1"\?>\s*' r'?\s*<modeling>' r'?\s*<generator>' r'?\s*<i name="program" type="string">\s*vasp\s*</i>' r'?'), - supported_compressions=['gz', 'bz2'] + supported_compressions=['gz', 'bz2', 'xz'] ), VaspOutcarParser( name='parsers/vasp-outcar', code_name='VASP', diff --git a/nomad/search.py b/nomad/search.py index 9a85add85b111602c668292dcfcf553e7c297163..5e9f4a9aee30b41841d34060058786daa5fc4127 100644 --- a/nomad/search.py +++ b/nomad/search.py @@ -319,6 +319,13 @@ class SearchRequest: value = quantity.elastic_value(value) + if quantity.elastic_search_type == 'terms': + if not isinstance(value, list): + value = [value] + self.q &= Q('terms', **{quantity.elastic_field: value}) + + return self + if isinstance(value, list): values = value else: diff --git a/ops/helm/nomad/Chart.yaml b/ops/helm/nomad/Chart.yaml index 787b7894f5acc792e2a839d0b16d7688dba45eac..ee30ccb632c53cd7c521425500ca5681f75e14ec 100644 --- a/ops/helm/nomad/Chart.yaml +++ b/ops/helm/nomad/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: "0.6.0" +appVersion: "0.7.0" description: A Helm chart for Kubernetes that only runs nomad services and uses externally hosted databases. name: nomad -version: 0.6.0 +version: 0.7.0 diff --git a/ops/helm/nomad/templates/gui-deployment.yml b/ops/helm/nomad/templates/gui-deployment.yml index b7eff504f4d55c99463c591bff631b7d0f7094da..3c18d78dac167e8a718d3b5bdfd79d6a88b6bcd6 100644 --- a/ops/helm/nomad/templates/gui-deployment.yml +++ b/ops/helm/nomad/templates/gui-deployment.yml @@ -39,6 +39,16 @@ data: rewrite ^{{ .Values.proxy.external.path }}/gui/service-worker.js /nomad/service-worker.js break; } + location {{ .Values.proxy.external.path }}/gui/meta.json { + add_header Last-Modified $date_gmt; + add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + if_modified_since off; + expires off; + etag off; + root /app/; + rewrite ^{{ .Values.proxy.external.path }}/gui/meta.json /nomad/meta.json break; + } + location {{ .Values.proxy.external.path }}/api/uploads { client_max_body_size 35g; proxy_request_buffering off; diff --git a/setup.py b/setup.py index 3b733e4bd72422fe3a735548bf7c7e921e04e2cf..f151c837a5795a71030c088acc46165fbaa9e437 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ reqs = [str(ir.req) for ir in install_reqs if 'sphinxcontrib.httpdomain' not in setup( name='nomad', - version='0.6.0', + version='0.7.0', description='The nomad@FAIRDI infrastructure python package', py_modules=['nomad'], install_requires=reqs, diff --git a/tests/app/test_api.py b/tests/app/test_api.py index 7960221992062e28baa7d69d12d0b49ed15d5a11..fc5516db179cdf20f48c7935489f9a5d2e9b3861 100644 --- a/tests/app/test_api.py +++ b/tests/app/test_api.py @@ -600,19 +600,19 @@ class TestRepo(): calc_with_metadata.update( calc_id='2', uploader=other_test_user.user_id, published=True, with_embargo=False, pid=2, upload_time=today - datetime.timedelta(days=5), - external_id='external_id') + external_id='external_2') calc_with_metadata.update( atoms=['Fe'], comment='this is a specific word', formula='AAA', basis_set='zzz') search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True) calc_with_metadata.update( calc_id='3', uploader=other_test_user.user_id, published=False, - with_embargo=False, pid=3, external_id='external_id') + with_embargo=False, pid=3, external_id='external_3') search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True) calc_with_metadata.update( calc_id='4', uploader=other_test_user.user_id, published=True, - with_embargo=True, pid=4, external_id='external_id') + with_embargo=True, pid=4, external_id='external_4') search.Entry.from_calc_with_metadata(calc_with_metadata).save(refresh=True) def assert_search(self, rv: Any, number_of_calcs: int) -> dict: @@ -711,30 +711,35 @@ class TestRepo(): rv = api.get('/repo/%s' % query_string) self.assert_search(rv, calcs) - @pytest.mark.parametrize('calcs, quantity, value', [ - (2, 'system', 'bulk'), - (0, 'system', 'atom'), - (1, 'atoms', 'Br'), - (1, 'atoms', 'Fe'), - (0, 'atoms', ['Fe', 'Br', 'A', 'B']), - (0, 'only_atoms', ['Br', 'Si']), - (1, 'only_atoms', ['Fe']), - (1, 'only_atoms', ['Br', 'K', 'Si']), - (1, 'only_atoms', ['Br', 'Si', 'K']), - (1, 'comment', 'specific'), - (1, 'authors', 'Leonard Hofstadter'), - (2, 'files', 'test/mainfile.txt'), - (2, 'paths', 'mainfile.txt'), - (2, 'paths', 'test'), - (2, 'quantities', ['wyckoff_letters_primitive', 'hall_number']), - (0, 'quantities', 'dos'), - (1, 'external_id', 'external_id'), - (0, 'external_id', 'external') + @pytest.mark.parametrize('calcs, quantity, value, user', [ + (2, 'system', 'bulk', 'test_user'), + (0, 'system', 'atom', 'test_user'), + (1, 'atoms', 'Br', 'test_user'), + (1, 'atoms', 'Fe', 'test_user'), + (0, 'atoms', ['Fe', 'Br', 'A', 'B'], 'test_user'), + (0, 'only_atoms', ['Br', 'Si'], 'test_user'), + (1, 'only_atoms', ['Fe'], 'test_user'), + (1, 'only_atoms', ['Br', 'K', 'Si'], 'test_user'), + (1, 'only_atoms', ['Br', 'Si', 'K'], 'test_user'), + (1, 'comment', 'specific', 'test_user'), + (1, 'authors', 'Hofstadter, Leonard', 'test_user'), + (2, 'files', 'test/mainfile.txt', 'test_user'), + (2, 'paths', 'mainfile.txt', 'test_user'), + (2, 'paths', 'test', 'test_user'), + (2, 'quantities', ['wyckoff_letters_primitive', 'hall_number'], 'test_user'), + (0, 'quantities', 'dos', 'test_user'), + (2, 'external_id', 'external_2,external_3', 'other_test_user'), + (1, 'external_id', 'external_2', 'test_user'), + (1, 'external_id', 'external_2,external_3', 'test_user'), + (0, 'external_id', 'external_x', 'test_user') ]) - def test_search_parameters(self, api, example_elastic_calcs, no_warn, test_user_auth, calcs, quantity, value): + def test_search_parameters( + self, api, example_elastic_calcs, no_warn, test_user_auth, + other_test_user_auth, calcs, quantity, value, user): + user_auth = test_user_auth if user == 'test_user' else other_test_user_auth query_string = urlencode({quantity: value, 'statistics': True}, doseq=True) - rv = api.get('/repo/?%s' % query_string, headers=test_user_auth) + rv = api.get('/repo/?%s' % query_string, headers=user_auth) logger.debug('run search quantities test', query_string=query_string) data = self.assert_search(rv, calcs)