"""Define an object that represents a database project."""
import multiprocessing.pool
import runpy
import typing
from dataclasses import dataclass, field
from pathlib import Path
from ase.db.core import Database
from asr.database import connect
KeyDescriptions = typing.Dict[str, typing.Tuple[str, str, str]]
def args2query(args):
return args["query"]
def make_layout_function():
from asr.database.browser import layout
return layout
def row_to_dict(row, project):
from asr.database.app import Summary
def create_layout(*args, **kwargs):
return project.layout_function(*args, pool=project.pool, **kwargs)
project_name = project["name"]
uid = row.get(project["uid_key"])
prefix = str(project.tmpdir / f"{project_name}/{uid}-") if project.tmpdir else None
s = Summary(
row,
create_layout=create_layout,
key_descriptions=project["key_descriptions"],
prefix=prefix,
)
return s
def make_default_key_descriptions(db=None):
from asr.database.app import create_default_key_descriptions
return create_default_key_descriptions(db=db)
[docs]@dataclass
class DatabaseProject:
"""Class that represents a database project.
Parameters
----------
name
The name of the database project.
title
The title of the database object.
database
A database connection
uid_key
Key to be used as unique identifier.
key_descriptions
Key descriptions used by the web application
tmpdir
Path to temporary directory used by project to store files.
row_to_dict_function
A function that takes (row, project) as input and produces an
object (normally a dict) that is handed to the row template
also specified in this project.
handle_query_function
A function that takes a query tuple and returns a query tuple.
Useful for doing translations when the query uses aliases
for values, for example to convert stability=low to stability=1.
default_columns
Default columns that the application should show on the search page.
table_template
Path to the table jinja-template.
The table template shows the rows of the database.
row_template
Path to the row jinja-template.
The row template is responsible for showing a detailed description
of a particular row.
search_template
Path to the search jinja-template. The search template embeds the table
template and is responsible for formatting the search field.
layout function
Function used by the defuault row_to_dict_function to create columns,
figures, tables etc.
pool
Processes used for generating figures. If False, then figures are produced
by the main process. If None, then a pool is created by automatically.
template_search_path
Path that will be added to flask's template search paths where the row, search
and/or table template should be located. If None, ASR/ASE assumes the templates
to be located at default locations within ASE or ASR in that order.
"""
name: str
title: str
database: Database
uid_key: str = "uid"
key_descriptions: "KeyDescriptions" = field(
default_factory=make_default_key_descriptions
)
tmpdir: typing.Optional[Path] = None
row_to_dict_function: typing.Callable = row_to_dict
handle_query_function: typing.Callable = args2query
default_columns: typing.List[str] = field(
default_factory=lambda: list(["formula", "id"])
)
table_template: str = "asr/database/templates/table.html"
row_template: str = "asr/database/templates/row.html"
search_template: str = "asr/database/templates/search.html"
layout_function: typing.Callable = field(default_factory=make_layout_function)
pool: typing.Union[bool, None, multiprocessing.pool.Pool] = None
template_search_path: typing.Optional[str] = None
# ASE project handling requires that the project is indexable,
# so we implement getitem to integrate with ASE.
def __getitem__(self, item):
return self.__dict__[item]
[docs] @classmethod
def from_pyfile(cls, path: str) -> "DatabaseProject":
"""Make a database project from a Python file.
The project is constructed from the variables defined in the input
python script. The extracted variable names are the same as as the
parameters to the :class:`asr.database.DatabaseProject` constructor.
Parameters
----------
path : str
Path to a Python file that defines some or all of
the attributes that defines a database project, e.g.
name=, title=. At a minimum `name`, `title` and `database` needs
to be defined.
Returns
-------
DatabaseProject
A database project constructed from the attributes defined
in the python file.
Examples
--------
A minimal valid python script to define a database project looks like
.. code-block:: python
from ase.db import connect
name = "Name of my database"
title = "Title of my database"
database = connect("path/to/my/database.db")
"""
dct = runpy.run_path(str(path))
kwargs_for_constructor = {}
th = typing.get_type_hints(cls)
keys_allowed_for_project_spec = set(th.keys())
for key in keys_allowed_for_project_spec:
if key in dct:
kwargs_for_constructor[key] = dct[key]
return cls(**kwargs_for_constructor)
[docs] @classmethod
def from_database(cls, path: str) -> "DatabaseProject":
"""Make a database project from an ASE database.
The project construction acquires project attributes from the database
metadata. These includes the `name`, `title`, `uid`, `default_columns`,
`table_template`, `search_template`, `row_template`. Additionally, the
project construction requires that the database metadata contains a key
named `keys` whose value is a list of strings for which key-descriptions
should be generated from a default set of key descriptions.
If `name` is not specified then the filename is used. If `title` is not
specified then it is set to the same value as `name`.
Parameters
----------
path : str
Path to an ASE database with the metadata outlined above.
Returns
-------
DatabaseProject
A database project constructed from the input database.
Examples
--------
A minimal valid metadata examples looks like
.. code-block:: json
{
"name": "Name of my database",
"keys": ["formula", "natoms"],
}
"""
db = connect(path)
metadata = db.metadata
name = metadata.get("name", Path(path).name)
key_descriptions = make_default_key_descriptions(db)
extract_keys = {
"uid",
"default_columns",
"table_template",
"search_template",
"row_template",
}
kwargs_for_constructor = dict(
name=name, database=db, key_descriptions=key_descriptions,
)
for key in extract_keys:
if key in metadata:
kwargs_for_constructor[key] = metadata[key]
# To mirror previous behaviour, title is treated specially
if "title" not in kwargs_for_constructor:
kwargs_for_constructor["title"] = kwargs_for_constructor["name"]
return cls(**kwargs_for_constructor)