diff --git a/nomad/app/v1/models/graph/__main__.py b/nomad/app/v1/models/graph/__main__.py deleted file mode 100644 index 30ebdc1e7e9164ea056a8a05482fd5c12c17bda5..0000000000000000000000000000000000000000 --- a/nomad/app/v1/models/graph/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -import sys - -module_prefix = 'nomad.app.v1.models.graph' - -setattr(sys.modules[f'{module_prefix}.utils'], 'ref_prefix', '#/definitions') -model = getattr(sys.modules[f'{module_prefix}.graph_models'], sys.argv[1]) -print(model.schema_json(indent=2)) diff --git a/nomad/app/v1/models/graph/graph_models.py b/nomad/app/v1/models/graph/graph_models.py index ff4b2151c21999bbcf866d2c235e589bbba52142..f9eaac851f050b923dbe983b357fd86d555fa2b0 100644 --- a/nomad/app/v1/models/graph/graph_models.py +++ b/nomad/app/v1/models/graph/graph_models.py @@ -184,6 +184,7 @@ class GraphUploads(BaseModel): class GraphEntryMetadata(BaseModel, extra=Extra.allow): entry: GraphEntry + m_children: Any class SearchRequestOptions(BaseModel): diff --git a/nomad/app/v1/models/graph/utils.py b/nomad/app/v1/models/graph/utils.py index b32a291b08aa81ee806fbaab6fa4687bd607123a..bd26f6fc09925f3f459046da28092cd9c0f3fdba 100644 --- a/nomad/app/v1/models/graph/utils.py +++ b/nomad/app/v1/models/graph/utils.py @@ -52,6 +52,7 @@ import sys ref_prefix = '#/components/schemas' request_suffix = 'Request' response_suffix = 'Response' +graph_model_export = False class _DictModel(BaseModel): @@ -110,6 +111,14 @@ class _DictModel(BaseModel): if value_type == Literal['*']: types.append({'enum': ['*'], 'type': 'string'}) else: + # This forces all model names to be unique. Pydandic + # replaces non unique model names with qualified names. + # We are just using the plain name here. Unfortunately, + # there is no way to get the "long_model_name" map from pydantic + # to put the right names here. Therefore, in the presence + # of non-unique names, we are using the wrong referenes. + # Things depending on the openapi schema will cause issues. + # i.e. https://gitlab.mpcdf.mpg.de/nomad-lab/nomad-FAIR/-/issues/1958 types.append({'$ref': f'{ref_prefix}/{value_type.__name__}'}) if 'properties' in schema: @@ -196,6 +205,13 @@ def _generate_model( # We need to populate a forward ref for the model in the ns use it in recursion cases. result_model_name = f'{source_model.__name__}{suffix}' + if ( + graph_model_export + and result_model_name.startswith('Graph') + and result_model_name not in ['GraphRequest', 'GraphResponse'] + ): + result_model_name = result_model_name[5:] + is_ns_origin = len(ns) == 0 if result_model_name not in ns: ns[result_model_name] = ForwardRef(result_model_name) @@ -307,6 +323,7 @@ def _generate_model( assert ( getattr(sys.modules[source_model.__module__], result_model_name, result_model) == result_model + or graph_model_export ), f'Model class with name {result_model_name} already exists.' setattr(sys.modules[source_model.__module__], result_model_name, result_model) diff --git a/nomad/app/v1/routers/entries.py b/nomad/app/v1/routers/entries.py index beae96be75da9ff752b7353755305d7e2cfb4f4a..c921c0edbc4dbea73f04fe2245936da63b662c55 100644 --- a/nomad/app/v1/routers/entries.py +++ b/nomad/app/v1/routers/entries.py @@ -329,6 +329,14 @@ class ArchiveChange(BaseModel): new_value: Any action: ArchiveChangeAction = ArchiveChangeAction.upsert + class Config: + @staticmethod + def schema_extra(schema, model) -> None: + # Removing the title from the Any typed new_value field + # makes this a proper JSON schema "any" instead of a named + # empty type. + del schema['properties']['new_value']['title'] + class EntryEdit(BaseModel): changes: List[ArchiveChange] @@ -1485,9 +1493,10 @@ async def post_entry_edit( # - no checks yet, we simply assume that the raw file and the changes # agree on the schema # - no handling of concurrent changes yet - for change in reversed(data.changes): + for change in data.changes: path = change.path.split('/') section_data = archive_data + next_key = to_key(path[0]) for path_index, path_segment in enumerate(path[:-1]): # Usually all keys are str and indicate either a quantity or diff --git a/nomad/cli/dev.py b/nomad/cli/dev.py index 9aaadbbf6108b4872a909fbbb56b49e48ef30fb1..9bf52df4bf4db658be37d55e52d7ebf056d39a70 100644 --- a/nomad/cli/dev.py +++ b/nomad/cli/dev.py @@ -78,6 +78,36 @@ def gui_qa(skip_tests: bool): sys.exit(ret_code) +@dev.command(help='Export an API model in JSON schema.') +@click.argument('model') +def api_model(model): + import importlib + + if model in [ + 'nomad.app.v1.models.graph.GraphRequest', + 'nomad.app.v1.models.graph.GraphResponse', + ]: + from nomad.app.v1.models.graph.utils import ( + generate_request_model, + generate_response_model, + ) + from nomad.app.v1.models.graph.graph_models import Graph + + sys.modules['nomad.app.v1.models.graph.utils'].ref_prefix = '#/definitions' + sys.modules['nomad.app.v1.models.graph.utils'].graph_model_export = True + + if model == 'nomad.app.v1.models.graph.GraphRequest': + model = generate_request_model(Graph) + else: + model = generate_response_model(Graph) + print(model.schema_json(indent=2)) + else: + pkg, cls = model.rsplit('.', 1) + importlib.import_module(pkg) + model = getattr(sys.modules[pkg], cls) + print(model.schema_json(indent=2)) + + def get_gui_artifacts_js() -> str: from nomad.datamodel import all_metainfo_packages from nomad.parsing.parsers import code_metadata diff --git a/nomad/parsing/parser.py b/nomad/parsing/parser.py index 680826748397e0214787bc9864f228a158b618ae..7e95da9792f2155e97e8bb1a0ed79b2b344c474b 100644 --- a/nomad/parsing/parser.py +++ b/nomad/parsing/parser.py @@ -527,9 +527,11 @@ class ArchiveParser(MatchingParser): if metadata_data is not None: self.domain = metadata_data.get('domain') - # Setting metadata in this way is not supported (any more) - if entry_name := metadata_data.get('entry_name', None): - archive.metadata.entry_name = entry_name + for quantity_name in ['entry_name', 'references', 'comment']: + quantity = EntryMetadata.m_def.all_quantities[quantity_name] + if value := metadata_data.get(quantity_name, None): + archive.metadata.m_set(quantity, value) + del archive_data[EntryArchive.metadata.name] # ensure that definitions are parsed first to make them available for the diff --git a/tests/app/v1/routers/test_entries_archive_edit.py b/tests/app/v1/routers/test_entries_archive_edit.py index 557e0bf6eca9e3ebccbad3e7d89c9eabb0fd44b6..8a2e90107dbffd9b62c3d1d9677d5627e4ed7559 100644 --- a/tests/app/v1/routers/test_entries_archive_edit.py +++ b/tests/app/v1/routers/test_entries_archive_edit.py @@ -67,8 +67,8 @@ from tests.test_files import create_test_upload_files pytest.param( { 'changes': [ - {'path': 'data/sub', 'action': 'remove'}, {'path': 'data/sub/name', 'new_value': 'NewName'}, + {'path': 'data/sub', 'action': 'remove'}, ] }, { @@ -82,14 +82,40 @@ from tests.test_files import create_test_upload_files pytest.param( { 'changes': [ - {'path': 'data/sub/1', 'action': 'remove'}, {'path': 'data/sub/1/name', 'new_value': 'NewName'}, + {'path': 'data/sub/1', 'action': 'remove'}, ] }, {'data': {'name': 'TestName', 'sub': [None]}}, 'user1', id='remove-repeated-sub-section', ), + pytest.param( + { + 'changes': [ + {'path': 'data/sub/0', 'action': 'upsert', 'new_value': {}}, + { + 'path': 'data/sub/0/name', + 'action': 'upsert', + 'new_value': 'NewName1', + }, + {'path': 'data/sub/1', 'action': 'upsert', 'new_value': {}}, + { + 'path': 'data/sub/1/name', + 'action': 'upsert', + 'new_value': 'NewName2', + }, + ] + }, + { + 'data': { + 'name': 'TestName', + 'sub': [{'name': 'NewName1'}, {'name': 'NewName2'}], + } + }, + 'user1', + id='add-multiple-repeated-sub-section', + ), ], ) def test_post_entry_edit(