Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
nomad-lab
nomad-FAIR
Commits
83190be9
Commit
83190be9
authored
Dec 23, 2020
by
Markus Scheidgen
Browse files
Added optimade-python-tools to NOMADs fastapi api.
parent
73c75007
Changes
33
Hide whitespace changes
Inline
Side-by-side
MANIFEST.in
View file @
83190be9
recursive-include dependencies/optimade-python-tools *.txt *.g *.py *.ini
recursive-include dependencies/optimade-python-tools *.txt *.g *.py *.ini
*.json
recursive-include nomad *.json *.j2 *.md *.yaml
include nomad/units/*.txt
include README.md
...
...
gui/public/env.js
View file @
83190be9
...
...
@@ -3,7 +3,7 @@ window.nomadEnv = {
'
keycloakRealm
'
:
'
fairdi_nomad_test
'
,
'
keycloakClientId
'
:
'
nomad_gui_dev
'
,
'
appBase
'
:
'
http://nomad-lab.eu/prod/rae/beta
'
,
'
appBase
'
:
'
http://localhost:8000
'
,
'
appBase
'
:
'
http://localhost:8000
/fairdi/nomad/latest
'
,
'
debug
'
:
false
,
'
matomoEnabled
'
:
false
,
'
matomoUrl
'
:
'
https://nomad-lab.eu/fairdi/stat
'
,
...
...
gui/src/config.js
View file @
83190be9
...
...
@@ -23,7 +23,7 @@ export const appBase = window.nomadEnv.appBase.replace(/\/$/, '')
// export const apiBase = 'http://nomad-lab.eu/prod/rae/api'
export
const
apiBase
=
`
${
appBase
}
/api`
export
const
apiV1Base
=
`
${
appBase
}
/api/v1`
export
const
optimade
Base
=
`
${
appBase
}
/optimade`
export
const
optimade
Api
=
`
${
appBase
}
/optimade
/v1/extensions/docs
`
export
const
guiBase
=
process
.
env
.
PUBLIC_URL
export
const
matomoUrl
=
window
.
nomadEnv
.
matomoUrl
export
const
matomoSiteId
=
window
.
nomadEnv
.
matomoSiteId
...
...
nomad/app/__init__.py
View file @
83190be9
...
...
@@ -36,7 +36,6 @@ import orjson
from
nomad
import
config
,
utils
as
nomad_utils
from
.api
import
blueprint
as
api_blueprint
,
api
from
.optimade
import
blueprint
as
optimade_blueprint
,
api
as
optimade
from
.dcat
import
blueprint
as
dcat_blueprint
from
.docs
import
blueprint
as
docs_blueprint
from
.dist
import
blueprint
as
dist_blueprint
...
...
@@ -70,7 +69,6 @@ def output_json(data, code, headers=None):
api
.
representation
(
'application/json'
)(
output_json
)
optimade
.
representation
(
'application/json'
)(
output_json
)
@
property
# type: ignore
...
...
@@ -100,7 +98,6 @@ app.config['SECRET_KEY'] = config.services.api_secret
CORS
(
app
)
app
.
register_blueprint
(
api_blueprint
,
url_prefix
=
'/api'
)
app
.
register_blueprint
(
optimade_blueprint
,
url_prefix
=
'/optimade'
)
app
.
register_blueprint
(
dcat_blueprint
,
url_prefix
=
'/dcat'
)
app
.
register_blueprint
(
docs_blueprint
,
url_prefix
=
'/docs'
)
app
.
register_blueprint
(
dist_blueprint
,
url_prefix
=
'/dist'
)
...
...
nomad/app/api/common.py
View file @
83190be9
...
...
@@ -35,7 +35,7 @@ import gzip
from
functools
import
wraps
from
nomad
import
search
,
config
,
datamodel
,
utils
from
nomad.app.optimade
import
filterparser
from
nomad.app
_fastapi
.optimade
import
filterparser
from
nomad.app.common
import
RFC3339DateTime
,
rfc3339DateTime
from
nomad.files
import
Restricted
...
...
nomad/app/optimade/__init__.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
'''
The optimade implementation of NOMAD.
'''
from
flask
import
Blueprint
from
flask_restplus
import
Api
from
.api
import
blueprint
,
url
,
api
from
.structures
import
Structure
,
StructuresInfo
,
StructureList
from
.calculations
import
CalculationList
,
Calculation
,
CalculationInfo
from
.infolinks
import
Info
,
Links
from
.index
import
Info
from
.filterparser
import
parse_filter
nomad/app/optimade/api.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
from
flask
import
Blueprint
from
flask_restplus
import
Api
import
urllib.parse
from
nomad
import
config
blueprint
=
Blueprint
(
'optimade'
,
__name__
)
base_url
=
'https://%s/%s/optimade'
%
(
config
.
services
.
api_host
.
strip
(
'/'
),
config
.
services
.
api_prefix
.
strip
(
'/'
))
def
url
(
endpoint
:
str
=
None
,
version
=
'v1'
,
prefix
=
None
,
**
kwargs
):
''' Returns the full optimade api url (for a given endpoint) including query parameters. '''
if
endpoint
is
not
None
:
url
=
'/'
+
endpoint
else
:
url
=
''
if
version
is
not
None
:
url
=
'/'
+
version
+
url
if
prefix
is
not
None
:
url
=
'/'
+
prefix
+
url
url
=
base_url
+
url
if
len
(
kwargs
)
>
0
:
return
'%s?%s'
%
(
url
,
urllib
.
parse
.
urlencode
(
kwargs
))
else
:
return
url
api
=
Api
(
blueprint
,
version
=
'1.0'
,
title
=
'NOMAD
\'
s OPTiMaDe API implementation'
,
description
=
'NOMAD
\'
s OPTiMaDe API implementation, version 1.0.0.'
,
validate
=
True
)
''' Provides the flask restplust api instance for the optimade api'''
# For some unknown reason it is necessary for each fr api to have a handler.
# Otherwise the global app error handler won't be called.
@
api
.
errorhandler
(
Exception
)
def
errorhandler
(
error
):
'''When an internal server error is caused by an unexpected exception.'''
return
str
(
error
)
nomad/app/optimade/calculations.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
from
flask_restplus
import
Resource
,
abort
from
flask
import
request
from
nomad.datamodel
import
OptimadeEntry
from
.api
import
api
from
.common
import
base_request_args
,
base_search_request
,
nentries
,
ns
from
.models
import
json_api_single_response_model
,
entry_listing_endpoint_parser
,
Meta
,
\
Links
as
LinksModel
,
single_entry_endpoint_parser
,
base_endpoint_parser
,
\
json_api_info_response_model
,
json_api_list_response_model
,
EntryDataObject
,
\
get_entry_properties
,
to_calc_with_metadata
from
.filterparser
import
parse_filter
,
FilterException
@
ns
.
route
(
'/calculations'
)
class
CalculationList
(
Resource
):
@
api
.
doc
(
'calculations'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
expect
(
entry_listing_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_list_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
):
''' Returns a list of calculations that match the given optimade filter expression. '''
response_fields
=
base_request_args
()
try
:
filter
=
request
.
args
.
get
(
'filter'
,
None
)
page_limit
=
int
(
request
.
args
.
get
(
'page_limit'
,
10
))
page_number
=
int
(
request
.
args
.
get
(
'page_number'
,
1
))
sort
=
request
.
args
.
get
(
'sort'
,
request
.
args
.
get
(
'sort'
,
'chemical_formula_reduced'
))
order
=
1
if
sort
[
0
:
1
]
==
'-'
:
order
=
-
1
sort
=
sort
[
1
:]
except
Exception
:
abort
(
400
,
message
=
'bad parameter types'
)
# TODO Specific json API error handling
sort_quantity
=
OptimadeEntry
.
m_def
.
all_quantities
.
get
(
sort
,
None
)
if
sort_quantity
is
None
:
abort
(
400
,
message
=
'cannot sort by %s'
%
sort
)
# TODO Specific json API error handling
sort_quantity_a_optimade
=
sort_quantity
.
m_get_annotations
(
'optimade'
)
if
not
sort_quantity_a_optimade
.
sortable
:
abort
(
400
,
message
=
'cannot sort by %s'
%
sort
)
# TODO Specific json API error handling
search_request
=
base_search_request
().
include
(
'calc_id'
,
'upload_id'
)
if
filter
is
not
None
:
try
:
search_request
.
query
(
parse_filter
(
filter
))
except
FilterException
as
e
:
abort
(
400
,
message
=
'Could not parse filter expression: %s'
%
str
(
e
))
result
=
search_request
.
execute_paginated
(
page
=
page_number
,
per_page
=
page_limit
,
order
=
order
,
order_by
=
'dft.optimade.%s'
%
sort
)
returned
=
result
[
'pagination'
][
'total'
]
results
=
to_calc_with_metadata
(
result
[
'results'
])
assert
len
(
results
)
==
len
(
result
[
'results'
]),
'archive and elasticsearch are not consistent'
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
returned
,
available
=
nentries
(),
last_id
=
results
[
-
1
].
calc_id
if
len
(
results
)
>
0
else
None
),
links
=
LinksModel
(
'calculations'
,
returned
=
returned
,
page_number
=
page_number
,
page_limit
=
page_limit
,
sort
=
sort
,
filter
=
filter
),
data
=
[
EntryDataObject
(
d
,
optimade_type
=
'calculations'
,
response_fields
=
response_fields
)
for
d
in
results
]
),
200
@
ns
.
route
(
'/calculations/<string:id>'
)
class
Calculation
(
Resource
):
@
api
.
doc
(
'calculation'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
response
(
404
,
'Id does not exist.'
)
@
api
.
expect
(
single_entry_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_single_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
,
id
:
str
):
''' Retrieve a single calculation for the given id '''
response_fields
=
base_request_args
()
search_request
=
base_search_request
().
search_parameters
(
calc_id
=
id
)
result
=
search_request
.
execute_paginated
(
page
=
1
,
per_page
=
1
)
available
=
result
[
'pagination'
][
'total'
]
results
=
to_calc_with_metadata
(
result
[
'results'
])
assert
len
(
results
)
==
len
(
result
[
'results'
]),
'Mongodb and elasticsearch are not consistent'
if
available
==
0
:
abort
(
404
,
'The calculation with id %s does not exist'
%
id
)
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
1
),
data
=
EntryDataObject
(
results
[
0
],
optimade_type
=
'calculations'
,
response_fields
=
response_fields
)
),
200
@
ns
.
route
(
'/info/calculations'
)
class
CalculationInfo
(
Resource
):
@
api
.
doc
(
'calculations_info'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
expect
(
base_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_info_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
):
''' Returns information about the calculation endpoint implementation '''
base_request_args
()
result
=
{
'description'
:
'a calculation entry'
,
'properties'
:
get_entry_properties
(
include_optimade
=
False
),
'formats'
:
[
'json'
],
'output_fields_by_format'
:
{
'json'
:
list
(
OptimadeEntry
.
m_def
.
all_properties
.
keys
())}
}
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
1
),
data
=
result
),
200
nomad/app/optimade/common.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
from
flask_restplus
import
abort
from
flask
import
request
from
elasticsearch_dsl
import
Q
from
cachetools
import
cached
,
TTLCache
from
nomad
import
search
from
.api
import
api
ns
=
api
.
namespace
(
'v1'
,
description
=
'The version v1 API namespace with all OPTiMaDe endpoints.'
)
# TODO replace with decorator that filters response_fields
def
base_request_args
():
if
request
.
args
.
get
(
'response_format'
,
'json'
)
!=
'json'
:
abort
(
400
,
'Response format is not supported.'
)
properties_str
=
request
.
args
.
get
(
'response_fields'
,
None
)
if
properties_str
is
not
None
:
return
properties_str
.
split
(
','
)
return
None
def
base_search_request
():
''' Creates a search request for all public and optimade enabled data. '''
return
search
.
SearchRequest
().
owner
(
'public'
,
None
).
search_parameter
(
'processed'
,
True
).
query
(
Q
(
'exists'
,
field
=
'dft.optimade.elements'
))
# TODO use the elastic annotations when done
@
cached
(
TTLCache
(
maxsize
=
1
,
ttl
=
60
*
60
))
def
nentries
():
''' Gives the overall number of public calculations. '''
return
search
.
SearchRequest
().
owner
(
owner_type
=
'public'
).
execute
()[
'total'
]
nomad/app/optimade/index.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
from
flask_restplus
import
Resource
from
flask
import
request
from
nomad
import
config
from
.api
import
api
,
url
from
.common
import
base_request_args
from
.models
import
json_api_single_response_model
,
base_endpoint_parser
,
json_api_single_response_model
,
Meta
,
json_api_list_response_model
ns
=
api
.
namespace
(
'index/v1'
,
description
=
'This is the OPTiMaDe index for NOMAD
\'
implementations.'
)
@
ns
.
route
(
'/info'
)
class
Info
(
Resource
):
@
api
.
doc
(
'index_info'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
expect
(
base_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_single_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
):
''' Returns information relating to the API implementation- '''
base_request_args
()
result
=
{
'type'
:
'info'
,
'id'
:
'/'
,
'attributes'
:
{
'api_version'
:
'1.0.0'
,
'available_api_versions'
:
[{
'url'
:
url
(
prefix
=
'index'
),
'version'
:
'1.0.0'
}],
'formats'
:
[
'json'
],
'entry_types_by_format'
:
{
'json'
:
[
'links'
,
'info'
]
},
'available_endpoints'
:
[
'links'
,
'info'
],
'is_index'
:
True
},
'relationships'
:
{
'default'
:
{
'data'
:
{
'id'
:
'v1'
,
'type'
:
'links'
}
}
}
}
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
1
),
data
=
result
),
200
@
ns
.
route
(
'/links'
)
class
Links
(
Resource
):
@
api
.
doc
(
'index_links'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
expect
(
base_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_list_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
):
''' Returns information relating to the API implementation- '''
base_request_args
()
result
=
[
{
"type"
:
"child"
,
"id"
:
"v1"
,
"attributes"
:
{
"name"
:
config
.
meta
.
name
,
"description"
:
config
.
meta
.
description
,
"base_url"
:
{
"href"
:
url
(
version
=
None
),
},
"homepage"
:
config
.
meta
.
homepage
}
}
]
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
1
),
data
=
result
),
200
nomad/app/optimade/infolinks.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
from
flask_restplus
import
Resource
from
flask
import
request
from
nomad
import
config
from
.api
import
api
,
url
from
.common
import
ns
,
base_request_args
from
.models
import
json_api_single_response_model
,
base_endpoint_parser
,
Meta
,
\
json_api_list_response_model
@
ns
.
route
(
'/info'
)
class
Info
(
Resource
):
@
api
.
doc
(
'info'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
expect
(
base_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_single_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
):
''' Returns information about this optimade implementation '''
base_request_args
()
result
=
{
'type'
:
'info'
,
'id'
:
'/'
,
'attributes'
:
{
'api_version'
:
'1.0.0'
,
'available_api_versions'
:
[{
'url'
:
url
(),
'version'
:
'1.0.0'
}],
'formats'
:
[
'json'
],
'entry_types_by_format'
:
{
'json'
:
[
'structures'
,
'calculations'
]
},
'available_endpoints'
:
[
'structures'
,
'calculations'
,
'info'
],
'is_index'
:
False
}
}
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
1
),
data
=
result
),
200
@
ns
.
route
(
'/links'
)
class
Links
(
Resource
):
@
api
.
doc
(
'links'
)
@
api
.
response
(
400
,
'Invalid requests, e.g. bad parameter.'
)
@
api
.
expect
(
base_endpoint_parser
,
validate
=
True
)
@
api
.
marshal_with
(
json_api_list_response_model
,
skip_none
=
True
,
code
=
200
)
def
get
(
self
):
''' Returns information about related optimade databases '''
base_request_args
()
result
=
[
{
"type"
:
"links"
,
"id"
:
"index"
,
"attributes"
:
{
"name"
:
config
.
meta
.
name
,
"description"
:
config
.
meta
.
description
,
"link_type"
:
"root"
,
"base_url"
:
{
"href"
:
url
(
version
=
None
,
prefix
=
'index'
),
},
"homepage"
:
config
.
meta
.
homepage
}
}
]
return
dict
(
meta
=
Meta
(
query
=
request
.
url
,
returned
=
1
),
data
=
result
),
200
nomad/app/optimade/models.py
deleted
100644 → 0
View file @
73c75007
#
# Copyright The NOMAD Authors.
#
# This file is part of NOMAD. See https://nomad-lab.eu for further info.
#
# 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.
#
'''
All the API flask restplus models.
'''
from
typing
import
Set
,
List
,
Dict
,
Any
from
flask_restplus
import
fields
import
datetime
import
math
from
cachetools
import
cached
import
numpy
as
np
from
nomad
import
config
,
datamodel
,
files
from
nomad.app.common
import
RFC3339DateTime
from
nomad.normalizing.optimade
import
optimade_chemical_formula_reduced
from
nomad.metainfo
import
Datetime
,
MEnum
,
MSection
from
nomad.datamodel
import
EntryMetadata
from
nomad.datamodel.dft
import
DFTMetadata
from
nomad.datamodel.optimade
import
OptimadeEntry
from
.api
import
api
,
url
# TODO error/warning objects
json_api_meta_object_model
=
api
.
model
(
'MetaObject'
,
{
'query'
:
fields
.
Nested
(
model
=
api
.
model
(
'Query'
,
{
'representation'
:
fields
.
String
(
required
=
True
,
description
=
'A string with the part of the URL following the base URL'
)
},
description
=
'Information on the query that was requested.'
)),
'api_version'
:
fields
.
String
(