diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f482431d3..b68d1ee16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,5 @@ -# Install pre-commit hooks via +# # Install pre-commit hooks via # pre-commit install - -# modernizer: make sure our code-base is Python 3 ready -- repo: https://github.com/python-modernize/python-modernize.git - sha: a234ce4e185cf77a55632888f1811d83b4ad9ef2 - hooks: - - id: python-modernize - exclude: ^docs/ - args: - - --write - - --nobackups - repo: local hooks: diff --git a/.prospector.yaml b/.prospector.yaml new file mode 100644 index 000000000..30673782e --- /dev/null +++ b/.prospector.yaml @@ -0,0 +1,19 @@ +max-line-length: 120 + +ignore-paths: + - doc + - examples + - test + - utils + +pylint: + run: true + +pyflakes: + run: false + +pep8: + run: false + +mccabe: + run: false diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..87a432065 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,422 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore= + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=bad-continuation,locally-disabled,useless-suppression,django-not-available,bad-option-value + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=5 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,40})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_,pk + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,40})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,40})|(_[a-z0-9_]*)|(setUp)|(tearDown))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_,setUp,tearDown + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +# This is a pylint issue https://github.com/PyCQA/pylint/issues/73 +ignored-modules=distutils + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=6 + +# Maximum number of attributes for a class (see R0902). +max-attributes=12 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=20 + +# Maximum number of parents for a class (see R0901). +max-parents=20 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 000000000..b3d849f2d --- /dev/null +++ b/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style = google +column_limit = 120 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..2fe860ecf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: +- "3.7" + +before_install: + # Upgrade pip setuptools and wheel + - pip install -U wheel setuptools coveralls + +install: +- pip install -e .[testing,pre-commit] +- reentry scan -r aiida + +script: + - pre-commit install; pre-commit run --all-files || ( git status --short; git diff ; exit 1 ); diff --git a/README.md b/README.md index 88eaa334d..a92810cf5 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ pip install aiidalab-widgets-base Install the corresponding `aiidalab-widgets-base` AiiDA lab application via the app manager as usual. +### Optional dependencies + +* The `SmilesWidget` widget requires the [OpenBabel](http://openbabel.org/) library. + ## Usage Using the widgets usually just involves importing and displaying them. diff --git a/aiida_datatypes.ipynb b/aiida_datatypes_viewers.ipynb similarity index 61% rename from aiida_datatypes.ipynb rename to aiida_datatypes_viewers.ipynb index 2f5a0dcd5..cf38f5ac4 100644 --- a/aiida_datatypes.ipynb +++ b/aiida_datatypes_viewers.ipynb @@ -1,5 +1,16 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%aiida" + ] + }, { "cell_type": "code", "execution_count": null, @@ -7,11 +18,16 @@ "outputs": [], "source": [ "from os import path\n", - "from aiida import load_dbenv, is_dbenv_loaded\n", - "from aiida.backends import settings\n", - "if not is_dbenv_loaded():\n", - " load_dbenv(profile=settings.AIIDADB_PROFILE)\n", - "from aiida.orm import DataFactory" + "import numpy\n", + "from aiida.plugins import DataFactory\n", + "from aiidalab_widgets_base import viewer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## TrajectoryData" ] }, { @@ -20,14 +36,69 @@ "metadata": {}, "outputs": [], "source": [ - "from aiidalab_widgets_base import aiidalab_display" + "# visualize TrajectoryData\n", + "TrajectoryData = DataFactory('array.trajectory')\n", + "\n", + "traj = TrajectoryData()\n", + "\n", + "stepids = numpy.array([60, 70])\n", + "times = stepids * 0.01\n", + "cells = numpy.array([\n", + " [\n", + " [2., 0., 0.,], \n", + " [0., 2., 0.,],\n", + " [0., 0., 2.,]\n", + " ],\n", + " [\n", + " [3., 0., 0.,],\n", + " [0., 3., 0.,],\n", + " [0., 0., 3.,]\n", + " ]\n", + "])\n", + "symbols = ['H', 'O', 'H']\n", + "positions = numpy.array([\n", + " [\n", + " [0., 0., 0.],\n", + " [0.5, 0.5, 0.5],\n", + " [1.5, 1.5, 1.5]\n", + " ],\n", + " [\n", + " [0., 0., 0.],\n", + " [0.5, 0.5, 0.5],\n", + " [1.5, 1.5, 1.5]],\n", + "])\n", + "velocities = numpy.array([\n", + " [\n", + " [0., 0., 0.],\n", + " [0., 0., 0.],\n", + " [0., 0., 0.]\n", + " ],\n", + " [\n", + " [0.5, 0.5, 0.5],\n", + " [0.5, 0.5, 0.5],\n", + " [-0.5, -0.5, -0.5]\n", + " ]\n", + "])\n", + "\n", + "energy = numpy.array([1., 2.])\n", + "# I set the node\n", + "traj.set_trajectory(\n", + " stepids=stepids, cells=cells, symbols=symbols, positions=positions, times=times,\n", + " velocities=velocities\n", + ")\n", + "traj.set_array('energy', energy)\n", + "\n", + "traj.store()\n", + "vwr = viewer(traj)\n", + "#vwr = viewer(load_node('9bfc006b'))\n", + "display(vwr)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## ParameterData" + "## Dict" ] }, { @@ -36,15 +107,15 @@ "metadata": {}, "outputs": [], "source": [ - "# visualize ParameterData\n", - "ParameterData = DataFactory('parameter')\n", - "p = ParameterData(dict={\n", + "Dict = DataFactory('dict')\n", + "p = Dict(dict={\n", " 'Parameter' :'super long string '*4,\n", " 'parameter 2' :'value 2',\n", " 'parameter 3' : 1,\n", " 'parameter 4' : 2,\n", "})\n", - "aiidalab_display(p.store(), downloadable=True)" + "vwr = viewer(p.store(), downloadable=True)\n", + "display(vwr)" ] }, { @@ -72,10 +143,10 @@ "metadata": {}, "outputs": [], "source": [ - "# visualize CifData\n", "CifData = DataFactory('cif')\n", "s = CifData(ase=m)\n", - "aiidalab_display(s.store(), downloadable=True)" + "vwr = viewer(s.store(), downloadable=True)\n", + "display(vwr)" ] }, { @@ -91,10 +162,10 @@ "metadata": {}, "outputs": [], "source": [ - "# visualize StructureData\n", "StructureData = DataFactory('structure')\n", "s = StructureData(ase=m)\n", - "aiidalab_display(s.store(), downloadable=True)" + "vwr = viewer(s.store(), downloadable=True)\n", + "display(vwr)" ] }, { @@ -145,16 +216,9 @@ " (5, u'X'),\n", " (6, u'Z'),\n", " (11, u'U')]\n", - "bs.labels = labels" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "aiidalab_display(bs.store()) # to visualize the bands" + "bs.labels = labels\n", + "vwr = viewer(bs.store(), downloadable=True)\n", + "display(vwr)" ] }, { @@ -172,33 +236,34 @@ "source": [ "FolderData = DataFactory('folder')\n", "fd = FolderData()\n", - "with fd.folder.open(path.join('path','test1.txt'), 'w') as fobj:\n", - " fobj.write('content of test1 file')\n", - "with fd.folder.open(path.join('path','test2.txt'), 'w') as fobj:\n", - " fobj.write('content of test2\\nfile')\n", - "with fd.folder.open(path.join('path','test_long.txt'), 'w') as fobj:\n", - " fobj.write('content of test_long file'*1000)\n", - "aiidalab_display(fd.store(), downloadable=True)" + "with fd.open('test1.txt', 'w') as fobj:\n", + " fobj.write(u'content of test1 file')\n", + "with fd.open('test2.txt', 'w') as fobj:\n", + " fobj.write(u'content of test2\\nfile')\n", + "with fd.open('test_long.txt', 'w') as fobj:\n", + " fobj.write(u'content of test_long file'*1000)\n", + "vwr = viewer(fd.store(), downloadable=True)\n", + "display(vwr)" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.15rc1" + "pygments_lexer": "ipython3", + "version": "3.6.8" } }, "nbformat": 4, diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 7a6b6e8a0..4eed4421f 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -1,15 +1,19 @@ +"""Reusable widgets for AiiDA lab applications.""" # pylint: disable=unused-import -from aiida import load_dbenv, is_dbenv_loaded -from aiida.backends import settings -if not is_dbenv_loaded(): - load_dbenv(profile=settings.AIIDADB_PROFILE) +from aiida import load_profile +load_profile() -from .codes import CodeDropdown, AiiDACodeSetup, extract_aiidacodesetup_arguments # noqa -from .computers import SshComputerSetup, extract_sshcomputersetup_arguments # noqa -from .computers import AiidaComputerSetup, extract_aiidacomputer_arguments # noqa -from .databases import CodQueryWidget # noqa -from .display import aiidalab_display # noqa -from .structures import StructureUploadWidget # noqa +from .codes import CodeDropdown, AiiDACodeSetup, valid_aiidacode_args # noqa +from .computers import SshComputerSetup, valid_sshcomputer_args # noqa +from .computers import AiidaComputerSetup, valid_aiidacomputer_args # noqa +from .computers import ComputerDropdown # noqa +from .databases import CodQueryWidget # noqa +#from .editors import editor # noqa +from .export import ExportButtonWidget # noqa +from .process import ProcessFollowerWidget, ProgressBarWidget, RunningCalcJobOutputWidget, SubmitButtonWidget # noqa +from .structures import StructureManagerWidget # noqa +from .structures import StructureBrowserWidget, StructureExamplesWidget, StructureUploadWidget, SmilesWidget # noqa from .structures_multi import MultiStructureUploadWidget # noqa +from .viewers import viewer # noqa -__version__ = "0.4.0b2" +__version__ = "1.0.0a7" diff --git a/aiidalab_widgets_base/aiida_visualizers.py b/aiidalab_widgets_base/aiida_visualizers.py deleted file mode 100644 index 3f7f396a1..000000000 --- a/aiidalab_widgets_base/aiida_visualizers.py +++ /dev/null @@ -1,149 +0,0 @@ -from __future__ import print_function -import os - -import ipywidgets as ipw - -class ParameterDataVisualizer(ipw.HTML): - """Visualizer class for ParameterData object""" - def __init__(self, parameter, downloadable=True, **kwargs): - super(ParameterDataVisualizer, self).__init__(**kwargs) - import pandas as pd - # Here we are defining properties of 'df' class (specified while exporting pandas table into html). - # Since the exported object is nothing more than HTML table, all 'standard' HTML table settings - # can be applied to it as well. - # For more information on how to controle the table appearance please visit: - # https://css-tricks.com/complete-guide-table-element/ - self.value = ''' - - ''' - pd.set_option('max_colwidth', 40) - df = pd.DataFrame([(key, value) for key, value - in sorted(parameter.get_dict().items()) - ], columns=['Key', 'Value']) - self.value += df.to_html(classes='df', index=False) # specify that exported table belongs to 'df' class - # this is used to setup table's appearance using CSS - if downloadable: - import base64 - payload = base64.b64encode(df.to_csv(index=False).encode()).decode() - fname = '{}.csv'.format(parameter.pk) - to_add = """Download table in csv format: {title}""" - self.value += to_add.format(filename=fname, payload=payload,title=fname) - -class StructureDataVisualizer(ipw.VBox): - """Visualizer class for StructureData object""" - def __init__(self, structure, downloadable=True, **kwargs): - import nglview - self._structure = structure - viewer = nglview.NGLWidget() - viewer.add_component(nglview.ASEStructure(self._structure.get_ase())) # adds ball+stick - viewer.add_unitcell() - children = [viewer] - if downloadable: - self.file_format = ipw.Dropdown( - options=['xyz', 'cif'], - description="File format:", - ) - self.download_btn = ipw.Button(description="Download") - self.download_btn.on_click(self.download) - children.append(ipw.HBox([self.file_format, self.download_btn])) - super(StructureDataVisualizer, self).__init__(children, **kwargs) - - def download(self, b=None): - import base64 - from tempfile import TemporaryFile - from IPython.display import Javascript - with TemporaryFile() as fobj: - self._structure.get_ase().write(fobj, format=self.file_format.value) - fobj.seek(0) - b64 = base64.b64encode(fobj.read()) - payload = b64.decode() - js = Javascript( - """ - var link = document.createElement('a'); - link.href = "data:;base64,{payload}" - link.download = "{filename}" - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - """.format(payload=payload,filename=str(self._structure.id)+'.'+self.file_format.value) - ) - display(js) - -class FolderDataVisualizer(ipw.VBox): - """Visualizer class for FolderData object""" - def __init__(self, folder, downloadable=True, **kwargs): - self._folder = folder - self.files = ipw.Dropdown( - options=self._folder.get_folder_list(), - description="Select file:", - ) - self.text = ipw.Textarea( - value="", - description='File content:', - layout={'width':"900px", 'height':'300px'}, - disabled=False - ) - self.change_file_view() - self.files.observe(self.change_file_view, names='value') - children = [self.files, self.text] - if downloadable: - self.download_btn = ipw.Button(description="Download") - self.download_btn.on_click(self.download) - children.append(self.download_btn) - super(FolderDataVisualizer, self).__init__(children, **kwargs) - - def change_file_view(self, b=None): - with open(self._folder.get_abs_path(self.files.value), "rb") as fobj: - self.text.value = fobj.read() - - def download(self, b=None): - import base64 - from IPython.display import Javascript - with open(self._folder.get_abs_path(self.files.value), "rb") as fobj: - b64 = base64.b64encode(fobj.read()) - payload = b64.decode() - js = Javascript( - """ - var link = document.createElement('a'); - link.href = "data:;base64,{payload}" - link.download = "{filename}" - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - """.format(payload=payload,filename=self.files.value) - ) - display(js) - -class BandsDataVisualizer(ipw.VBox): - """Visualizer class for BandsData object""" - def __init__(self, bands, **kwargs): - from bokeh.io import show, output_notebook - from bokeh.models import Span - from bokeh.plotting import figure - output_notebook(hide_banner=True) - out = ipw.Output() - with out: - plot_info = bands._get_bandplot_data(cartesian=True, join_symbol="|") - # Extract relevant data - y = plot_info['y'].transpose().tolist() - x = [plot_info['x'] for i in range(len(y))] - labels = plot_info['labels'] - # Create the figure - p = figure(y_axis_label='Dispersion ({})'.format(bands.units)) - p.multi_line(x, y, line_width=2, line_color='red') - p.xaxis.ticker = [l[0] for l in labels] - # This trick was suggested here: https://github.com/bokeh/bokeh/issues/8166#issuecomment-426124290 - p.xaxis.major_label_overrides = {int(l[0]) if l[0].is_integer() else l[0]:l[1] for l in labels} - # Add vertical lines - p.renderers.extend([Span(location=l[0], dimension='height', line_color='black', line_width=3) for l in labels]) - show(p) - children = [out] - super(BandsDataVisualizer, self).__init__(children, **kwargs) \ No newline at end of file diff --git a/aiidalab_widgets_base/codes.py b/aiidalab_widgets_base/codes.py index 61e758542..8d2155908 100644 --- a/aiidalab_widgets_base/codes.py +++ b/aiidalab_widgets_base/codes.py @@ -1,32 +1,27 @@ -from __future__ import print_function - -from __future__ import absolute_import -import ipywidgets as ipw +"""Module to manage AiiDA codes.""" from subprocess import check_output + +import ipywidgets as ipw from IPython.display import clear_output from aiida.orm import Code -VALID_AIIDA_CODE_SETUP_ARGUMETNS = {'label', 'selected_computer', 'plugin', 'description', - 'exec_path', 'prepend_text', 'append_text'} +from aiidalab_widgets_base.utils import predefine_settings, valid_arguments + +VALID_AIIDA_CODE_SETUP_ARGUMETNS = { + 'label', 'selected_computer', 'plugin', 'description', 'exec_path', 'prepend_text', 'append_text' +} -def valid_arguments(arguments, valid_arguments): - result = {} - for key, value in arguments.items(): - if key in valid_arguments: - if type(value) is tuple or type(value) is list: - result[key] = '\n'.join(value) - else: - result[key] = value - return result -def extract_aiidacodesetup_arguments(arguments): +def valid_aiidacode_args(arguments): return valid_arguments(arguments, VALID_AIIDA_CODE_SETUP_ARGUMETNS) class CodeDropdown(ipw.VBox): - def __init__(self, input_plugin, text='Select code:', **kwargs): + """Code selection widget.""" + + def __init__(self, input_plugin, text='Select code:', path_to_root='../', **kwargs): """ Dropdown for Codes for one input plugin. :param input_plugin: Input plugin of codes to show @@ -41,39 +36,40 @@ def __init__(self, input_plugin, text='Select code:', **kwargs): self.dropdown = ipw.Dropdown(description=text, disabled=True) self._btn_refresh = ipw.Button(description="Refresh", layout=ipw.Layout(width="70px")) self._btn_refresh.on_click(self.refresh) - # TODO: use base_url here - self._setup_another = ipw.HTML(value="""Setup new code""") + # FOR LATER: use base_url here, when it will be implemented in the appmode. + self._setup_another = ipw.HTML(value="""Setup new code""".format( + path_to_root=path_to_root, label=input_plugin, plugin=input_plugin)) self.output = ipw.Output() - children = [ipw.HBox([self.dropdown, self._btn_refresh, self._setup_another]), - self.output] + children = [ipw.HBox([self.dropdown, self._btn_refresh, self._setup_another]), self.output] super(CodeDropdown, self).__init__(children=children, **kwargs) self.refresh() - def _get_codes(self, input_plugin): + @staticmethod + def _get_codes(input_plugin): + """Query the list of available codes.""" from aiida.orm.querybuilder import QueryBuilder - from aiida.backends.utils import get_automatic_user + from aiida.orm import User from aiida.orm import Computer - current_user = get_automatic_user() - - qb = QueryBuilder() - qb.append( - Computer, filters={'enabled': True}, project=['*'], tag='computer') - qb.append( - Code, - filters={ - 'attributes.input_plugin': { - '==': input_plugin - }, - 'extras.hidden': { - "~==": True - } - }, - project=['*'], - has_computer='computer') - results = qb.all() + current_user = User.objects.get_default() + + querybuild = QueryBuilder() + querybuild.append(Computer, project=['*'], tag='computer') + querybuild.append(Code, + filters={ + 'attributes.input_plugin': { + '==': input_plugin + }, + 'extras.hidden': { + "~==": True + } + }, + project=['*'], + with_computer='computer') + results = querybuild.all() # only codes on computers configured for the current user results = [r for r in results if r[0].is_user_configured(current_user)] @@ -81,7 +77,8 @@ def _get_codes(self, input_plugin): codes = {"{}@{}".format(r[1].label, r[0].name): r[1] for r in results} return codes - def refresh(self, b=None): + def refresh(self, change=None): # pylint: disable=unused-argument + """Refresh available codes.""" with self.output: clear_output() self.codes = self._get_codes(self.input_plugin) @@ -90,32 +87,32 @@ def refresh(self, b=None): self.dropdown.options = options if not options: - print("No codes found for input plugin '{}'.".format( - self.input_plugin)) + print("No codes found for input plugin '{}'.".format(self.input_plugin)) self.dropdown.disabled = True else: self.dropdown.disabled = False @property def selected_code(self): + """Returns a selected code.""" try: return self.codes[self.dropdown.value] except KeyError: return None + class AiiDACodeSetup(ipw.VBox): """Class that allows to setup AiiDA code""" + def __init__(self, **kwargs): - from aiida.common.pluginloader import all_plugins + from aiida.plugins.entry_point import get_entry_point_names from aiidalab_widgets_base.computers import ComputerDropdown - style = {"description_width":"200px"} + style = {"description_width": "200px"} # list of widgets to be displayed - self._inp_code_label = ipw.Text(description="AiiDA code label:", - layout=ipw.Layout(width="500px"), - style=style) + self._inp_code_label = ipw.Text(description="AiiDA code label:", layout=ipw.Layout(width="500px"), style=style) self._computer = ComputerDropdown(layout={'margin': '0px 0px 0px 125px'}) @@ -124,7 +121,7 @@ def __init__(self, **kwargs): layout=ipw.Layout(width="500px"), style=style) - self._inp_code_plugin = ipw.Dropdown(options=sorted(all_plugins('calculations')), + self._inp_code_plugin = ipw.Dropdown(options=sorted(get_entry_point_names('aiida.calculations')), description="Code plugin:", layout=ipw.Layout(width="500px"), style=style) @@ -145,27 +142,23 @@ def __init__(self, **kwargs): self._btn_setup_code = ipw.Button(description="Setup code") self._btn_setup_code.on_click(self._setup_code) self._setup_code_out = ipw.Output() - children = [ipw.HBox([ipw.VBox([self._inp_code_label, - self._computer, - self._inp_code_plugin, - self._inp_code_description, - self._exec_path]), ipw.VBox([self._prepend_text, - self._append_text])]), - self._btn_setup_code, - self._setup_code_out, - ] + children = [ + ipw.HBox([ + ipw.VBox([ + self._inp_code_label, self._computer, self._inp_code_plugin, self._inp_code_description, + self._exec_path + ]), + ipw.VBox([self._prepend_text, self._append_text]) + ]), + self._btn_setup_code, + self._setup_code_out, + ] # Check if some settings were already provided - self._predefine_settings(**kwargs) + predefine_settings(self, **kwargs) super(AiiDACodeSetup, self).__init__(children, **kwargs) - def _predefine_settings(self, **kwargs): - for key, value in kwargs.items(): - if hasattr(self, key): - setattr(self, key, value) - else: - raise AttributeError("'{}' object has no attribute '{}'".format(self, key)) - - def _setup_code(self, b=None): + def _setup_code(self, change=None): # pylint: disable=unused-argument + """Setup an AiiDA code.""" with self._setup_code_out: clear_output() if self.label is None: @@ -175,7 +168,7 @@ def _setup_code(self, b=None): print("You did not specify absolute path to the executable") return if self.exists(): - print ("Code {}@{} already exists".format(self.label, self.selected_computer.name)) + print("Code {}@{} already exists".format(self.label, self.selected_computer.name)) return code = Code(remote_computer_exec=(self.selected_computer, self.exec_path)) code.label = self.label @@ -184,12 +177,13 @@ def _setup_code(self, b=None): code.set_prepend_text(self.prepend_text) code.set_append_text(self.append_text) code.store() - code._reveal() + code.reveal() full_string = "{}@{}".format(self.label, self.selected_computer.name) - print(check_output(['verdi', 'code', 'show', full_string])) + print(check_output(['verdi', 'code', 'show', full_string]).decode('utf-8')) def exists(self): - from aiida.common.exceptions import NotExistent, MultipleObjectsError + """Returns True if the code exists, returns False otherwise.""" + from aiida.common import NotExistent, MultipleObjectsError try: Code.get_from_string("{}@{}".format(self.label, self.selected_computer.name)) return True @@ -200,10 +194,9 @@ def exists(self): @property def label(self): - if len(self._inp_code_label.value.strip()) == 0: + if not self._inp_code_label.value.strip(): return None - else: - return self._inp_code_label.value + return self._inp_code_label.value @label.setter def label(self, label): diff --git a/aiidalab_widgets_base/computers.py b/aiidalab_widgets_base/computers.py index 96cb55eb8..fecfab822 100644 --- a/aiidalab_widgets_base/computers.py +++ b/aiidalab_widgets_base/computers.py @@ -1,250 +1,256 @@ -from __future__ import print_function - -import pexpect -import ipywidgets as ipw +"""All functionality needed to setup a computer.""" from os import path from copy import copy -from IPython.display import clear_output from subprocess import check_output, call + +import pexpect +import ipywidgets as ipw +from IPython.display import clear_output from traitlets import Int from aiida.orm import Computer -from aiida.backends.utils import get_automatic_user, get_backend_type -from aiida.common.exceptions import NotExistent -from aiida.transport.plugins.ssh import parse_sshconfig +from aiida.orm import User +from aiida.common import NotExistent +from aiida.transports.plugins.ssh import parse_sshconfig -if get_backend_type() == 'sqlalchemy': - from aiida.backends.sqlalchemy.models.authinfo import DbAuthInfo -else: - from aiida.backends.djsite.db.models import DbAuthInfo +from aiidalab_widgets_base.utils import predefine_settings, valid_arguments +STYLE = {"description_width": "200px"} VALID_SSH_COMPUTER_SETUP_ARGUMETNS = {'hostname', 'username', 'proxy_hostname', 'proxy_username'} -VALID_AIIDA_COMPUTER_SETUP_ARGUMETNS = {'name', 'hostname', 'description', 'workdir', 'mpirun_cmd', - 'ncpus', 'transport_type', 'scheduler', 'prepend_text', 'append_text'} - -def valid_arguments(arguments, valid_arguments): - result = {} - for key, value in arguments.items(): - if key in valid_arguments: - if type(value) is tuple or type(value) is list: - result[key] = '\n'.join(value) - else: - result[key] = value - return result +VALID_AIIDA_COMPUTER_SETUP_ARGUMETNS = { + 'name', 'hostname', 'description', 'workdir', 'mpirun_cmd', 'ncpus', 'transport_type', 'scheduler', 'prepend_text', + 'append_text' +} + -def extract_sshcomputersetup_arguments(arguments): +def valid_sshcomputer_args(arguments): return valid_arguments(arguments, VALID_SSH_COMPUTER_SETUP_ARGUMETNS) -def extract_aiidacomputer_arguments(arguments): + +def valid_aiidacomputer_args(arguments): return valid_arguments(arguments, VALID_AIIDA_COMPUTER_SETUP_ARGUMETNS) -class SshComputerSetup(ipw.VBox): - setup_counter = Int(0) # Traitlet to inform other widgets about changes + +class SshComputerSetup(ipw.VBox): # pylint: disable=too-many-instance-attributes + """Setup password-free access to a computer.""" + setup_counter = Int(0) # Traitlet to inform other widgets about changes + def __init__(self, **kwargs): - style = {"description_width":"200px"} computer_image = ipw.HTML('') - self._inp_username = ipw.Text(description="SSH username:", - layout=ipw.Layout(width="350px"), - style=style) - self._inp_password = ipw.Password(description="SSH password:", - layout=ipw.Layout(width="130px"), - style=style) + # Computer ssh settings + self._inp_username = ipw.Text(description="SSH username:", layout=ipw.Layout(width="350px"), style=STYLE) + self._inp_password = ipw.Password(description="SSH password:", layout=ipw.Layout(width="130px"), style=STYLE) self._inp_computer_hostname = ipw.Text(description="Computer name:", - layout=ipw.Layout(width="350px"), - style=style) - self._use_proxy = ipw.Checkbox(value=False, description='Use proxy') - self._use_proxy.observe(self.on_use_proxy_change, names='value') - + layout=ipw.Layout(width="350px"), + style=STYLE) # Proxy ssh settings + self._use_proxy = ipw.Checkbox(value=False, description='Use proxy') + self._use_proxy.observe(self.on_use_proxy_change, names='value') self._inp_proxy_address = ipw.Text(description="Proxy server address:", layout=ipw.Layout(width="350px"), - style=style) + style=STYLE) self._use_diff_proxy_username = ipw.Checkbox(value=False, - description='Use different username and password', - layout={'width': 'initial'}) + description='Use different username and password', + layout={'width': 'initial'}) self._use_diff_proxy_username.observe(self.on_use_diff_proxy_username_change, names='value') - self._inp_proxy_username = ipw.Text(value='', description="Proxy server username:", - layout=ipw.Layout(width="350px"), style=style) + layout=ipw.Layout(width="350px"), + style=STYLE) self._inp_proxy_password = ipw.Password(value='', description="Proxy server password:", layout=ipw.Layout(width="138px"), - style=style) + style=STYLE) + + # Setup ssh button and output self._btn_setup_ssh = ipw.Button(description="Setup ssh") self._btn_setup_ssh.on_click(self.on_setup_ssh) self._setup_ssh_out = ipw.Output() - # Check if some settings were already provided - self._predefine_settings(**kwargs) + # Check whether some settings were already provided + predefine_settings(self, **kwargs) # Defining widgets positions - computer_ssh_box = ipw.VBox([self._inp_computer_hostname, - self._inp_username, - self._inp_password, - self._use_proxy], - layout=ipw.Layout(width="400px")) - - self._proxy_user_password_box = ipw.VBox([self._inp_proxy_username, - self._inp_proxy_password], - layout={'visibility':'hidden'}) - - self._proxy_ssh_box = ipw.VBox([self._inp_proxy_address, - self._use_diff_proxy_username, - self._proxy_user_password_box], - layout = {'visibility':'hidden','width':'400px'}) - - children = [ipw.HBox([computer_image, computer_ssh_box, self._proxy_ssh_box]), - self._btn_setup_ssh, - self._setup_ssh_out] - + computer_ssh_box = ipw.VBox( + [self._inp_computer_hostname, self._inp_username, self._inp_password, self._use_proxy], + layout=ipw.Layout(width="400px")) + self._proxy_user_password_box = ipw.VBox([self._inp_proxy_username, self._inp_proxy_password], + layout={'visibility': 'hidden'}) + self._proxy_ssh_box = ipw.VBox( + [self._inp_proxy_address, self._use_diff_proxy_username, self._proxy_user_password_box], + layout={ + 'visibility': 'hidden', + 'width': '400px' + }) + + children = [ + ipw.HBox([computer_image, computer_ssh_box, self._proxy_ssh_box]), self._btn_setup_ssh, self._setup_ssh_out + ] super(SshComputerSetup, self).__init__(children, **kwargs) - def _predefine_settings(self, **kwargs): - for key, value in kwargs.items(): - if hasattr(self, key): - setattr(self, key, value) - else: - raise AttributeError("'{}' object has no attirubte '{}'".format(self, key)) - - def _ssh_keygen(self): - fn = path.expanduser("~/.ssh/id_rsa") - if not path.exists(fn): + @staticmethod + def _ssh_keygen(): + """Generate ssh key pair.""" + fname = path.expanduser("~/.ssh/id_rsa") + if not path.exists(fname): print("Creating ssh key pair") # returns non-0 if the key pair already exists - call(["ssh-keygen", "-f", fn, "-t", "rsa", "-N", ""]) + call(["ssh-keygen", "-f", fname, "-t", "rsa", "-b", "4096", "-m", "PEM", "-N", ""]) def is_host_known(self, hostname=None): + """Check if the host is known already.""" if hostname is None: hostname = self.hostname - fn = path.expanduser("~/.ssh/known_hosts") - if not path.exists(fn): + fname = path.expanduser("~/.ssh/known_hosts") + if not path.exists(fname): return False return call(["ssh-keygen", "-F", hostname]) == 0 - def _make_host_known(self, hostname, proxycmd=[]): - fn = path.expanduser("~/.ssh/known_hosts") - print("Adding keys from %s to %s"%(hostname, fn)) - hashes = check_output(proxycmd+["ssh-keyscan", "-H", hostname]) - with open(fn, "a") as f: - f.write(hashes) + @staticmethod + def _make_host_known(hostname, proxycmd=None): + """Add host information into known_hosts file.""" + proxycmd = [] if proxycmd is None else proxycmd + fname = path.expanduser("~/.ssh/known_hosts") + print("Adding keys from %s to %s" % (hostname, fname)) + hashes = check_output(proxycmd + ["ssh-keyscan", "-H", hostname]) + with open(fname, "a") as fobj: + fobj.write(hashes.decode("utf-8")) def can_login(self, silent=False): - if self.username is None: # if I can't find the username - I must fail + """Check if it is possible to login into the remote host.""" + if self.username is None: # if I can't find the username - I must fail return False - userhost = self.username+"@"+self.hostname + userhost = self.username + "@" + self.hostname if not silent: - print("Trying ssh "+userhost+"... ", end='') + print("Trying ssh " + userhost + "... ", end='') # With BatchMode on, no password prompt or other interaction is attempted, # so a connect that requires a password will fail. ret = call(["ssh", userhost, "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "true"]) if not silent: - print("Ok" if ret==0 else "Failed") - return ret==0 + print("Ok" if ret == 0 else "Failed") + return ret == 0 def is_in_config(self): - fn = path.expanduser("~/.ssh/config") - if not path.exists(fn): + """Check if the config file contains host information.""" + fname = path.expanduser("~/.ssh/config") + if not path.exists(fname): return False - cfglines = open(fn).read().split("\n") - return "Host "+self.hostname in cfglines + cfglines = open(fname).read().split("\n") + return "Host " + self.hostname in cfglines def _write_ssh_config(self, proxycmd=''): - fn = path.expanduser("~/.ssh/config") - print("Adding section to "+fn) - with open(fn, "a") as f: - f.write("Host "+self.hostname+"\n") - f.write("User "+self.username+"\n") + """Put host information into the config file.""" + fname = path.expanduser("~/.ssh/config") + print("Adding section to " + fname) + with open(fname, "a") as file: + file.write("Host " + self.hostname + "\n") + file.write("User " + self.username + "\n") if proxycmd: - f.write("ProxyCommand ssh -q -Y "+proxycmd+" netcat %h %p\n") - f.write("ServerAliveInterval 5\n") + file.write("ProxyCommand ssh -q -Y " + proxycmd + " netcat %h %p\n") + file.write("ServerAliveInterval 5\n") - def _send_pubkey(self, hostname, username, password, proxycmd=''): + @staticmethod + def _send_pubkey(hostname, username, password, proxycmd=''): + """Send a publick key to a remote host.""" from pexpect import TIMEOUT timeout = 10 - print("Sending public key to {}... ".format(hostname),end='') + print("Sending public key to {}... ".format(hostname), end='') str_ssh = 'ssh-copy-id {}@{}'.format(username, hostname) if proxycmd: - str_ssh += ' -o "ProxyCommand ssh -q -Y '+proxycmd+' netcat %h %p\n"' + str_ssh += ' -o "ProxyCommand ssh -q -Y ' + proxycmd + ' netcat %h %p\n"' child = pexpect.spawn(str_ssh) try: - index = child.expect(['s password:', # 0 - 'ERROR: No identities found', # 1 - 'All keys were skipped because they already exist on the remote system', # 2 - 'Could not resolve hostname', # 3 - pexpect.EOF],timeout=timeout) # final + index = child.expect( + [ + "s password:", # 0 + "All keys were skipped because they already exist on the remote system", # 1 + "ERROR: No identities found", # 2 + "Could not resolve hostname", # 3 + pexpect.EOF + ], + timeout=timeout) # final except TIMEOUT: - print ("Exceeded {} s timeout".format(timeout)) + print("Exceeded {} s timeout".format(timeout)) return False + possible_output = { + 1: { + 'message': "Keys are already present on the remote machine", + 'status': True + }, + 2: { + 'message': "Failed\nLooks like the key pair is not present in ~/.ssh folder", + 'status': False + }, + 3: { + 'message': "Failed\nUnknown hostname", + 'status': False + }, + } if index == 0: child.sendline(password) try: - child.expect("Now try logging into",timeout=5) - except: - print("Failed") - print("Please check your username and/or password") + child.expect("Now try logging into", timeout=5) + except TIMEOUT: + print("Failed\nPlease check your username and/or password") return False print("Ok") child.close() return True - elif index == 1: - print("Failed") - print("Looks like the key pair is not present in ~/.ssh folder") - return False - elif index == 2: - print("Keys are already there") - return True - elif index == 3: - print("Failed") - print("Unknown hostname") - return False - else: - print ("Failed") - print ("Unknown problem") - print (child.before, child.after) - child.close() - return False + + if index in possible_output: + print(possible_output[index]['message']) + return possible_output[index]['status'] + + print("Failed\nUnknown problem") + print(child.before, child.after) + child.close() + return False def _configure_proxy(self, password, proxy_password): + """Configure proxy server.""" # if proxy IS required if self._use_proxy.value: # again some standard checks if proxy server parameters are provided - if self.proxy_hostname is None: # hostname + if self.proxy_hostname is None: # hostname print("Please specify the proxy server hostname") return False, '' # if proxy username and password must be different from the main computer - they should be provided if self._use_diff_proxy_username.value: + # check username - if not self.proxy_username is None: + if self.proxy_username is not None: proxy_username = self.proxy_username else: print("Please specify the proxy server username") return False, '' + # check password - if len(proxy_password.strip()) == 0: + if not proxy_password.strip(): print("Please specify the proxy server password") return False, '' - else: # if username and password are the same as for the main computer + + else: # if username and password are the same as for the main computer proxy_username = self.username proxy_password = password # make proxy server known if not self.is_host_known(self.proxy_hostname): self._make_host_known(self.proxy_hostname) + # Finally trying to connect if self._send_pubkey(self.proxy_hostname, proxy_username, proxy_password): return True, proxy_username + '@' + self.proxy_hostname - else: - print ("Could not send public key to {} (proxy server).".format(self.proxy_hostname)) - return False, '' + + print("Could not send public key to {} (proxy server).".format(self.proxy_hostname)) + return False, '' + # if proxy is NOT required - else: - return True, '' + return True, '' - def on_setup_ssh(self, b): + def on_setup_ssh(self, change): # pylint: disable=unused-argument,too-many-return-statements """ATTENTION: modifying the order of operations in this function can lead to unexpected problems""" with self._setup_ssh_out: clear_output() @@ -255,47 +261,48 @@ def on_setup_ssh(self, b): proxy_password = self.__proxy_password # step 1: if hostname is not provided - do not do anything - if self.hostname is None: # check hostname + if self.hostname is None: # check hostname print("Please specify the computer hostname") return # step 2: check if password-free access was enabled earlier if self.can_login(): - print ("Password-free access is already enabled") + print("Password-free access is already enabled") # it can still happen that password-free access is enabled # but host is not present in the config file - fixing this if not self.is_in_config(): - self._write_ssh_config() # we do not use proxy here, because if computer + self._write_ssh_config() # we do not use proxy here, because if computer # can be accessed without any info in the config - proxy is not needed. - self.setup_counter += 1 # only if config file has changed - increase setup_counter + self.setup_counter += 1 # only if config file has changed - increase setup_counter return - # step 3: if can't login already, chek whether all required information is provided - if self.username is None: # check username + # step 3: if couldn't login in the previous step, chek whether all required information is provided + if self.username is None: # check username print("Please enter your ssh username") return - if len(password.strip()) == 0: # check password + if not password.strip(): # check password print("Please enter your ssh password") return # step 4: get the right commands to access the proxy server (if provided) success, proxycmd = self._configure_proxy(password, proxy_password) if not success: - return - + return + # step 5: make host known by ssh on the proxy server if not self.is_host_known(): - self._make_host_known(self.hostname,['ssh']+[proxycmd] if proxycmd else []) + self._make_host_known(self.hostname, ['ssh'] + [proxycmd] if proxycmd else []) # step 6: sending public key to the main host if not self._send_pubkey(self.hostname, self.username, password, proxycmd): - print ("Could not send public key to {}".format(self.hostname)) + print("Could not send public key to {}".format(self.hostname)) return # step 7: modify the ssh config file if necessary if not self.is_in_config(): self._write_ssh_config(proxycmd=proxycmd) - # TODO: add a check if new config is different from the current one. If so + + # FOR LATER: add a check if new config is different from the current one. If so # infrom the user about it. # step 8: final check @@ -303,25 +310,27 @@ def on_setup_ssh(self, b): self.setup_counter += 1 print("Automatic ssh setup successful :-)") return - else: - print("Automatic ssh setup failed, sorry :-(") - return + print("Automatic ssh setup failed, sorry :-(") + return - def on_use_proxy_change(self, b): + def on_use_proxy_change(self, change): # pylint: disable=unused-argument + """If proxy check-box is clicked.""" if self._use_proxy.value: self._proxy_ssh_box.layout.visibility = 'visible' else: self._proxy_ssh_box.layout.visibility = 'hidden' self._use_diff_proxy_username.value = False - def on_use_diff_proxy_username_change(self, b): + def on_use_diff_proxy_username_change(self, change): # pylint: disable=unused-argument + """If using different username for proxy check-box is clicked.""" if self._use_diff_proxy_username.value: self._proxy_user_password_box.layout.visibility = 'visible' else: self._proxy_user_password_box.layout.visibility = 'hidden' + # Keep this function only because it might be used later. -# What it does: looks inside .ssh/config file and loads computer setup from +# What it does: looks inside .ssh/config file and loads computer setup from # there (if present) # def _get_from_config(self, b): # config = parse_sshconfig(self.hostname) @@ -356,10 +365,9 @@ def __proxy_password(self): @property def hostname(self): - if len(self._inp_computer_hostname.value.strip()) == 0: # check hostname + if not self._inp_computer_hostname.value.strip(): # check hostname return None - else: - return self._inp_computer_hostname.value + return self._inp_computer_hostname.value @hostname.setter def hostname(self, hostname): @@ -368,15 +376,14 @@ def hostname(self, hostname): @property def username(self): """Loking for username in user's input and config file""" - if len(self._inp_username.value.strip()) == 0: # if username provided by user - if not self.hostname is None: + if not self._inp_username.value.strip(): # if username provided by user + if self.hostname is not None: config = parse_sshconfig(self.hostname) - if 'user' in config: # if username is present in the config file + if 'user' in config: # if username is present in the config file return config['user'] else: return None - else: - return self._inp_username.value + return self._inp_username.value @username.setter def username(self, username): @@ -384,10 +391,9 @@ def username(self, username): @property def proxy_hostname(self): - if len(self._inp_proxy_address.value.strip()) == 0: + if not self._inp_proxy_address.value.strip(): return None - else: - return self._inp_proxy_address.value + return self._inp_proxy_address.value @proxy_hostname.setter def proxy_hostname(self, proxy_hostname): @@ -396,10 +402,9 @@ def proxy_hostname(self, proxy_hostname): @property def proxy_username(self): - if len(self._inp_proxy_username.value.strip()) == 0: + if not self._inp_proxy_username.value.strip(): return None - else: - return self._inp_proxy_username.value + return self._inp_proxy_username.value @proxy_username.setter def proxy_username(self, proxy_username): @@ -407,60 +412,61 @@ def proxy_username(self, proxy_username): self._use_diff_proxy_username.value = True self._inp_proxy_username.value = proxy_username -class AiidaComputerSetup(ipw.VBox): + +class AiidaComputerSetup(ipw.VBox): # pylint: disable=too-many-instance-attributes + """Inform AiiDA about a computer.""" + def __init__(self, **kwargs): - from aiida.transport import Transport - from aiida.scheduler import Scheduler - style = {"description_width":"200px"} + from aiida.transports import Transport + from aiida.schedulers import Scheduler # list of widgets to be displayed - self._btn_setup_comp = ipw.Button(description="Setup computer") - self._btn_setup_comp.on_click(self._on_setup_computer) self._inp_computer_name = ipw.Text(value='', placeholder='Will only be used within AiiDA', description="AiiDA computer name:", layout=ipw.Layout(width="500px"), - style=style) + style=STYLE) self._computer_hostname = ipw.Dropdown(description="Select among configured hosts:", layout=ipw.Layout(width="500px"), - style=style) + style=STYLE) self._inp_computer_description = ipw.Text(value='', placeholder='No description (yet)', description="Computer description:", layout=ipw.Layout(width="500px"), - style=style) + style=STYLE) self._computer_workdir = ipw.Text(value='/scratch/{username}/aiida_run', description="Workdir:", layout=ipw.Layout(width="500px"), - style=style) + style=STYLE) self._computer_mpirun_cmd = ipw.Text(value='mpirun -n {tot_num_mpiprocs}', description="Mpirun command:", layout=ipw.Layout(width="500px"), - style=style) + style=STYLE) self._computer_ncpus = ipw.IntText(value=12, step=1, description='Number of CPU(s) per node:', layout=ipw.Layout(width="270px"), - style=style) + style=STYLE) self._transport_type = ipw.Dropdown(value='ssh', options=Transport.get_valid_transports(), description="Transport type:", - style=style) + style=STYLE) self._scheduler = ipw.Dropdown(value='slurm', options=Scheduler.get_valid_schedulers(), description="Scheduler:", - style=style) + style=STYLE) self._prepend_text = ipw.Textarea(placeholder='Text to prepend to each command execution', description='Prepend text:', - layout=ipw.Layout(width="400px") - ) + layout=ipw.Layout(width="400px")) self._append_text = ipw.Textarea(placeholder='Text to append to each command execution', description='Append text:', - layout=ipw.Layout(width="400px") - ) + layout=ipw.Layout(width="400px")) + + # Buttons and outputs + self._btn_setup_comp = ipw.Button(description="Setup computer") + self._btn_setup_comp.on_click(self._on_setup_computer) self._btn_test = ipw.Button(description="Test computer") self._btn_test.on_click(self.test) - self._setup_comp_out = ipw.Output(layout=ipw.Layout(width="500px")) self._test_out = ipw.Output(layout=ipw.Layout(width="500px")) @@ -468,38 +474,34 @@ def __init__(self, **kwargs): self.get_available_computers() # Check if some settings were already provided - self._predefine_settings(**kwargs) - children =[ipw.HBox([ipw.VBox([self._inp_computer_name, - self._computer_hostname, - self._inp_computer_description, - self._computer_workdir, - self._computer_mpirun_cmd, - self._computer_ncpus, - self._transport_type, - self._scheduler]), - ipw.VBox([self._prepend_text, - self._append_text])]), - ipw.HBox([self._btn_setup_comp, self._btn_test]), - ipw.HBox([self._setup_comp_out, self._test_out]), - ] + predefine_settings(self, **kwargs) + + # Organize the widgets + children = [ + ipw.HBox([ + ipw.VBox([ + self._inp_computer_name, self._computer_hostname, self._inp_computer_description, + self._computer_workdir, self._computer_mpirun_cmd, self._computer_ncpus, self._transport_type, + self._scheduler + ]), + ipw.VBox([self._prepend_text, self._append_text]) + ]), + ipw.HBox([self._btn_setup_comp, self._btn_test]), + ipw.HBox([self._setup_comp_out, self._test_out]), + ] super(AiidaComputerSetup, self).__init__(children, **kwargs) - def _predefine_settings(self, **kwargs): - for key, value in kwargs.items(): - if hasattr(self, key): - setattr(self, key, value) - else: - raise AttributeError("'{}' object has no attribute '{}'".format(self, key)) - - def get_available_computers(self, b=None): - fn = path.expanduser("~/.ssh/config") - if not path.exists(fn): + def get_available_computers(self, change=None): # pylint: disable=unused-argument + """Refresh the list of available computers.""" + fname = path.expanduser("~/.ssh/config") + if not path.exists(fname): return [] - cfglines = open(fn).readlines() + cfglines = open(fname).readlines() self._computer_hostname.options = [line.split()[1] for line in cfglines if 'Host' in line] + return True def _configure_computer(self): - """create DbAuthInfo""" + """create AuthInfo""" print("Configuring '{}'".format(self.name)) sshcfg = parse_sshconfig(self.hostname) authparams = { @@ -516,35 +518,34 @@ def _configure_computer(self): if 'user' in sshcfg: authparams['username'] = sshcfg['user'] else: - print ("SSH username is not provided, please run `verdi computer configure {}` " - "from the command line".format(self.name)) + print("SSH username is not provided, please run `verdi computer configure {}` " + "from the command line".format(self.name)) return if 'proxycommand' in sshcfg: authparams['proxy_command'] = sshcfg['proxycommand'] - aiidauser = get_automatic_user() - authinfo = DbAuthInfo(dbcomputer=Computer.get(self.name).dbcomputer, aiidauser=aiidauser) + aiidauser = User.objects.get_default() + from aiida.orm import AuthInfo + authinfo = AuthInfo(computer=Computer.objects.get(name=self.name), user=aiidauser) authinfo.set_auth_params(authparams) - authinfo.save() - print(check_output(['verdi', 'computer', 'show', self.name])) + authinfo.store() + print(check_output(['verdi', 'computer', 'show', self.name]).decode('utf-8')) - def _on_setup_computer(self, b): + def _on_setup_computer(self, change): # pylint: disable=unused-argument + """When setup computer button is pressed.""" with self._setup_comp_out: clear_output() - if self.name is None: # check hostname + if self.name is None: # check hostname print("Please specify the computer name (for AiiDA)") return try: - computer = Computer.get(self.name) + computer = Computer.objects.get(name=self.name) print("A computer called {} already exists.".format(self.name)) return except NotExistent: pass print("Creating new computer with name '{}'".format(self.name)) - computer = Computer(name=self.name) - computer.set_hostname(self.hostname) - computer.set_description(self.description) - computer.set_enabled_state(True) + computer = Computer(name=self.name, hostname=self.hostname, description=self.description) computer.set_transport_type(self.transport_type) computer.set_scheduler_type(self.scheduler) computer.set_workdir(self.workdir) @@ -557,17 +558,16 @@ def _on_setup_computer(self, b): computer.store() self._configure_computer() - def test(self, b=None): + def test(self, change): # pylint: disable=unused-argument with self._test_out: clear_output() - print(check_output(['verdi', 'computer', 'test', '--traceback', self.name])) + print(check_output(['verdi', 'computer', 'test', '--print-traceback', self.name]).decode('utf-8')) @property def name(self): - if len(self._inp_computer_name.value.strip()) == 0: # check hostname + if not self._inp_computer_name.value.strip(): # check hostname return None - else: - return self._inp_computer_name.value + return self._inp_computer_name.value @name.setter def name(self, name): @@ -575,10 +575,9 @@ def name(self, name): @property def hostname(self): - if self._computer_hostname.value is None or len(self._computer_hostname.value.strip()) == 0: # check hostname + if self._computer_hostname.value is None or not self._computer_hostname.value.strip(): # check hostname return None - else: - return self._computer_hostname.value + return self._computer_hostname.value @hostname.setter def hostname(self, hostname): @@ -625,6 +624,7 @@ def transport_type(self): def transport_type(self, transport_type): if transport_type in self._transport_type.options: self._transport_type.value = transport_type + @property def scheduler(self): return self._scheduler.value @@ -650,46 +650,52 @@ def append_text(self): def append_text(self, append_text): self._append_text.value = append_text + class ComputerDropdown(ipw.VBox): - def __init__(self, text='Select computer:', **kwargs): + """Widget to select a configured computer.""" + + def __init__(self, text='Select computer:', **kwargs): """ Dropdown for Codes for one input plugin. :param text: Text to display before dropdown :type text: str """ - self._dropdown = ipw.Dropdown(options=[], description=text, style={'description_width': 'initial'}, disabled=True) + self._dropdown = ipw.Dropdown(options=[], + description=text, + style={'description_width': 'initial'}, + disabled=True) self._btn_refresh = ipw.Button(description="Refresh", layout=ipw.Layout(width="70px")) - self._setup_another = ipw.HTML(value="""Setup new computer""", - layout = {'margin': '0px 0px 0px 250px'}) + self._setup_another = ipw.HTML( + value="""Setup new computer""", + layout={'margin': '0px 0px 0px 250px'}) self._btn_refresh.on_click(self._refresh) self.output = ipw.Output() - children = [ipw.HBox([self._dropdown, self._btn_refresh]), - self._setup_another, - self.output] + children = [ipw.HBox([self._dropdown, self._btn_refresh]), self._setup_another, self.output] super(ComputerDropdown, self).__init__(children=children, **kwargs) self._refresh() def _get_computers(self): + """Get the list of available computers.""" from aiida.orm.querybuilder import QueryBuilder - current_user = get_automatic_user() + current_user = User.objects.get_default() - qb = QueryBuilder() - qb.append( - Computer, filters={'enabled': True}, project=['*'], tag='computer') + query_b = QueryBuilder() + query_b.append(Computer, project=['*'], tag='computer') - results = qb.all() + results = query_b.all() # only computers configured for the current user results = [r for r in results if r[0].is_user_configured(current_user)] - self._dropdown.options = {r[0].name:r[0] for r in results} + self._dropdown.options = {r[0].name: r[0] for r in results} - def _refresh(self, b=None): + def _refresh(self, change=None): # pylint: disable=unused-argument + """Refresh the list of configured computers.""" with self.output: clear_output() self._get_computers() @@ -705,6 +711,7 @@ def computers(self): @property def selected_computer(self): + """Return selected computer.""" try: return self._dropdown.value except KeyError: @@ -714,4 +721,3 @@ def selected_computer(self): def selected_computer(self, selected_computer): if selected_computer in self.computers: self._dropdown.label = selected_computer - diff --git a/aiidalab_widgets_base/crystal_sim_crystal.py b/aiidalab_widgets_base/crystal_sim_crystal.py deleted file mode 100644 index c1bec38a5..000000000 --- a/aiidalab_widgets_base/crystal_sim_crystal.py +++ /dev/null @@ -1,56 +0,0 @@ -import itertools -import numpy as np -import spglib - -from ase.spacegroup import crystal -from ase.data import atomic_numbers, atomic_names -from ase.spacegroup import Spacegroup -from numpy.linalg import norm - - -def ase_to_spgcell(ase_atoms): - return (ase_atoms.get_cell(), - ase_atoms.get_scaled_positions(), - ase_atoms.get_atomic_numbers()) -def check_crystal_equivalence(crystal_a, crystal_b): - """Function that identifies whether two crystals are equivalent""" - - # getting symmetry datasets for both crystals - cryst_a = spglib.get_symmetry_dataset(ase_to_spgcell(crystal_a), symprec=1e-5, angle_tolerance=-1.0, hall_number=0) - cryst_b = spglib.get_symmetry_dataset(ase_to_spgcell(crystal_b), symprec=1e-5, angle_tolerance=-1.0, hall_number=0) - - samecell = np.allclose(cryst_a['std_lattice'], cryst_b['std_lattice'], atol=1e-5) - samenatoms = len(cryst_a['std_positions']) == len(cryst_b['std_positions']) - samespg = cryst_a['number'] == cryst_b['number'] - - def test_rotations_translations(cryst_a, cryst_b, repeat): - cell = cryst_a['std_lattice'] - pristine = crystal('Mg', [(0, 0., 0.)], - spacegroup=int(cryst_a['number']), - cellpar=[cell[0]/repeat[0], cell[1]/repeat[1], cell[2]/repeat[2]]).repeat(repeat) - - sym_set_p = spglib.get_symmetry_dataset(ase_to_spgcell(pristine), symprec=1e-5, - angle_tolerance=-1.0, hall_number=0) - - for _,trans in enumerate(zip(sym_set_p['rotations'], sym_set_p['translations'])): - pnew=(np.matmul(trans[0],cryst_a['std_positions'].T).T + trans[1]) % 1.0 - fulln = np.concatenate([cryst_a['std_types'][:, None], pnew], axis=1) - fullb = np.concatenate([cryst_b['std_types'][:, None], cryst_b['std_positions']], axis=1) - sorted_n = np.array(sorted([ list(row) for row in list(fulln) ])) - sorted_b = np.array(sorted([ list(row) for row in list(fullb) ])) - if np.allclose(sorted_n, sorted_b, atol=1e-5): - return True - return False - - if samecell and samenatoms and samespg: - cell = cryst_a['std_lattice'] - # we assume there are no crystals with a lattice parameter smaller than 2 A - rng1 = range(1, int(norm(cell[0])/2.)) - rng2 = range(1, int(norm(cell[1])/2.)) - rng3 = range(1, int(norm(cell[2])/2.)) - - for repeat in itertools.product(rng1, rng2, rng3): - if test_rotations_translations(cryst_a, cryst_b, repeat): - return True - - return False diff --git a/aiidalab_widgets_base/databases.py b/aiidalab_widgets_base/databases.py index b9c66c5b2..3398a0d6c 100644 --- a/aiidalab_widgets_base/databases.py +++ b/aiidalab_widgets_base/databases.py @@ -1,8 +1,9 @@ -from __future__ import print_function +"""Widgets that allow to query online databases.""" import ipywidgets as ipw from aiida.tools.dbimporters.plugins.cod import CodDbImporter + class CodQueryWidget(ipw.VBox): '''Query structures in Crystallography Open Database (COD) Useful class members: @@ -31,71 +32,83 @@ def __init__(self, **kwargs): For the queries by structure id, plese provide the database id number. Example: 1008786 """) layout = ipw.Layout(width="400px") - style = {"description_width":"initial"} - self.inp_elements = ipw.Text(description="", value="", placeholder='e.g.: Ni Ti or id number', layout=layout, style=style) + style = {"description_width": "initial"} + self.inp_elements = ipw.Text(description="", + value="", + placeholder='e.g.: Ni Ti or id number', + layout=layout, + style=style) self.btn_query = ipw.Button(description='Query') self.query_message = ipw.HTML("Waiting for input...") - self.drop_structure = ipw.Dropdown(description="", options=[("select structure",{"status":False})], - style=style, layout=layout ) + self.drop_structure = ipw.Dropdown(description="", + options=[("select structure", { + "status": False + })], + style=style, + layout=layout) self.link = ipw.HTML("Link to the web-page will appear here") self.structure_ase = None self.btn_query.on_click(self._on_click_query) self.drop_structure.observe(self._on_select_structure, names=['value']) - children = [description, - ipw.HBox([self.btn_query, self.inp_elements]), - self.query_message, - ipw.HBox([self.drop_structure, self.link])] + children = [ + description, + ipw.HBox([self.btn_query, self.inp_elements]), self.query_message, + ipw.HBox([self.drop_structure, self.link]) + ] super(CodQueryWidget, self).__init__(children=children, **kwargs) - def _query(self, idn=None,formula=None): + @staticmethod + def _query(idn=None, formula=None): + """Make the actual query.""" importer = CodDbImporter() if idn is not None: return importer.query(id=idn) - elif formula is not None: + if formula is not None: return importer.query(formula=formula) + return None - def _on_click_query(self, change): - structures = [("select structure", {"status":False})] + def _on_click_query(self, change): # pylint: disable=unused-argument + """Call query when the corresponding button is pressed.""" + structures = [("select structure", {"status": False})] idn = None formula = None self.query_message.value = "Quering the database ... " - try: + try: idn = int(self.inp_elements.value) - except: + except ValueError: formula = str(self.inp_elements.value) for entry in self._query(idn=idn, formula=formula): try: entry_cif = entry.get_cif_node() formula = entry_cif.get_ase().get_chemical_formula() - except: + except: # pylint: disable=bare-except continue - entry_add = ("{} (id: {})".format(formula, entry.source['id']), - { - "status": True, - "cif": entry_cif, - "url": entry.source['uri'], - "id": entry.source['id'], - } - ) + entry_add = ("{} (id: {})".format(formula, entry.source['id']), { + "status": True, + "cif": entry_cif, + "url": entry.source['uri'], + "id": entry.source['id'], + }) structures.append(entry_add) - self.query_message.value += "{} structures found".format(len(structures)-1) + self.query_message.value += "{} structures found".format(len(structures) - 1) self.drop_structure.options = structures def _on_select_structure(self, change): + """When a structure was selected.""" selected = change['new'] if selected['status'] is False: self.structure_ase = None return self.structure_ase = selected['cif'].get_ase() formula = self.structure_ase.get_chemical_formula() - struct_url = selected['url'].split('.cif')[0]+'.html' - self.link.value='COD entry {}'.format(struct_url, selected['id']) - if not self.on_structure_selection is None: + struct_url = selected['url'].split('.cif')[0] + '.html' + self.link.value = 'COD entry {}'.format(struct_url, selected['id']) + if self.on_structure_selection is not None: self.on_structure_selection(structure_ase=self.structure_ase, name=formula) - def on_structure_selection(self, structure_ase=None, name=None): + def on_structure_selection(self, structure_ase, name): pass diff --git a/aiidalab_widgets_base/display.py b/aiidalab_widgets_base/display.py deleted file mode 100644 index ce4f0489d..000000000 --- a/aiidalab_widgets_base/display.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import print_function - -import importlib -from IPython.display import display - -AIIDA_VISUALIZER_MAPPING = { - 'data.parameter.ParameterData.' : 'ParameterDataVisualizer', - 'data.structure.StructureData.' : 'StructureDataVisualizer', - 'data.cif.CifData.' : 'StructureDataVisualizer', - 'data.folder.FolderData.' : 'FolderDataVisualizer', - 'data.array.bands.BandsData.' : 'BandsDataVisualizer', -} - -def aiidalab_display(obj, downloadable=True, **kwargs): - """Display AiiDA data types in Jupyter notebooks. - - :param downloadable: If True, add link/button to download content of displayed AiiDA object. - - Defers to IPython.display.display for any objects it does not recognize. - """ - from aiidalab_widgets_base import aiida_visualizers - try: - visualizer = getattr(aiida_visualizers, AIIDA_VISUALIZER_MAPPING[obj.type]) - display(visualizer(obj, downloadable=downloadable), **kwargs) - except KeyError: - display(obj, **kwargs) \ No newline at end of file diff --git a/aiidalab_widgets_base/export.py b/aiidalab_widgets_base/export.py new file mode 100644 index 000000000..cee26b98f --- /dev/null +++ b/aiidalab_widgets_base/export.py @@ -0,0 +1,39 @@ +"""Widgets to manage AiiDA export.""" +import os +from ipywidgets import Button +from IPython.display import display + + +class ExportButtonWidget(Button): + """Export Node button.""" + + def __init__(self, process, **kwargs): + self.process = process + if 'description' not in kwargs: + kwargs['description'] = "Export workflow ({})".format(self.process.id) + if 'layout' not in kwargs: + kwargs['layout'] = {} + kwargs['layout']['width'] = 'initial' + super(ExportButtonWidget, self).__init__(**kwargs) + self.on_click(self.export_aiida_subgraph) + + def export_aiida_subgraph(self, change=None): # pylint: disable=unused-argument + """Perform export when the button is pressed""" + import base64 + import subprocess + from tempfile import mkdtemp + from IPython.display import Javascript + fname = os.path.join(mkdtemp(), 'export.aiida') + subprocess.call(['verdi', 'export', 'create', fname, '-N', str(self.process.id)]) + with open(fname, 'rb') as fobj: + b64 = base64.b64encode(fobj.read()) + payload = b64.decode() + javas = Javascript(""" + var link = document.createElement('a'); + link.href = "data:;base64,{payload}" + link.download = "{filename}" + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + """.format(payload=payload, filename='export_{}.aiida'.format(self.process.id))) + display(javas) diff --git a/aiidalab_widgets_base/misc.py b/aiidalab_widgets_base/misc.py new file mode 100644 index 000000000..2b2b03445 --- /dev/null +++ b/aiidalab_widgets_base/misc.py @@ -0,0 +1,39 @@ +"""Some useful classes used acrross the repository.""" + +import ipywidgets as ipw +from traitlets import Unicode + + +class CopyToClipboardButton(ipw.Button): + """Button to copy text to clipboard.""" + + value = Unicode(allow_none=True) # Traitlet that contains a string to copy. + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + super().on_click(self.copy_to_clipboard) + + def copy_to_clipboard(self, change=None): # pylint:disable=unused-argument + """Copy text to clipboard.""" + from IPython.display import Javascript, display + javas = Javascript(""" + function copyStringToClipboard (str) {{ + // Create new element + var el = document.createElement('textarea'); + // Set value (string to be copied) + el.value = str; + // Set non-editable to avoid focus and move outside of view + el.setAttribute('readonly', ''); + el.style = {{position: 'absolute', left: '-9999px'}}; + document.body.appendChild(el); + // Select text inside element + el.select(); + // Copy text to clipboard + document.execCommand('copy'); + // Remove temporary element + document.body.removeChild(el); + }} + copyStringToClipboard("{selection}"); + """.format(selection=self.value)) # For the moment works for Chrome, but doesn't work for Firefox. + if self.value: # If no value provided - do nothing. + display(javas) diff --git a/aiidalab_widgets_base/process.py b/aiidalab_widgets_base/process.py new file mode 100644 index 000000000..1cf427464 --- /dev/null +++ b/aiidalab_widgets_base/process.py @@ -0,0 +1,190 @@ +"""Widgets to work with processes.""" + +import os +from ipywidgets import Textarea, VBox + + +def get_running_calcs(process): + """Takes a process and returns a list of running calculations. The running calculations + can be either the process itself or its running children.""" + + from aiida.orm import CalcJobNode, ProcessNode, WorkChainNode + + # If a process is a running calculation - returning it + if issubclass(type(process), CalcJobNode) and not process.is_sealed: + return [process] + + # If the process is a running work chain - returning its children + if issubclass(type(process), WorkChainNode) and not process.is_sealed: + calcs = [] + for link in process.get_outgoing(): + if issubclass(type(link.node), ProcessNode) and not link.node.is_sealed: + calcs += get_running_calcs(link.node) + return calcs + # if it is neither calculation, nor work chain - returninng None + return [] + + +class SubmitButtonWidget(VBox): + """Submit button class that creates submit button jupyter widget.""" + + def __init__(self, process, widgets_values): + """Submit Button + :process: work chain to run + :param_funtion: the function that generates input parameters dictionary + """ + from ipywidgets import Button, Output + + self.process = None + self._process_class = process + self.widgets_values = widgets_values + self.btn_submit = Button(description="Submit", disabled=False) + self.btn_submit.on_click(self.on_btn_submit_press) + self.submit_out = Output() + children = [ + self.btn_submit, + self.submit_out, + ] + super(SubmitButtonWidget, self).__init__(children=children) + + def on_click(self, function): + self.btn_submit.on_click(function) + + def on_btn_submit_press(self, _): + """When submit button is pressed.""" + from IPython.display import clear_output + from aiida.engine import submit + + with self.submit_out: + clear_output() + self.btn_submit.disabled = True + input_dict = self.widgets_values() + self.process = submit(self._process_class, **input_dict) + print("Submitted process {}".format(self.process)) + return + + +class ProcessFollowerWidget(VBox): + """A Widget that follows a process until finished.""" + + def __init__(self, process, followers=None, update_interval=0.1, **kwargs): + """Initiate all the followers.""" + from aiida.orm import ProcessNode + + if not isinstance(process, ProcessNode): + raise TypeError("Expecting an object of type {}, got {}".format(ProcessNode, type(process))) + self.process = process + self.update_interval = update_interval + self.followers = [] + if followers is not None: + for follower in followers: + self.followers.append(follower(process=process)) + self.update() + super(ProcessFollowerWidget, self).__init__(children=self.followers, **kwargs) + + def update(self): + for follower in self.followers: + follower.update() + + def _follow(self): + """The loop that will update all the followers untill the process is running.""" + from time import sleep + while not self.process.is_sealed: + self.update() + sleep(self.update_interval) + self.update() # update the state for the last time to be 100% sure + + def follow(self, detach=False): + """Follow the process in blocking or non-blocking manner.""" + if detach: + import threading + update_state = threading.Thread(target=self._follow) + update_state.start() + else: + self._follow() + + +class ProgressBarWidget(VBox): + """A bar showing the proggress of a process.""" + + def __init__(self, process, **kwargs): + from ipywidgets import HTML, IntProgress, Layout + + self.process = process + self.correspondance = { + "created": 0, + "running": 1, + "waiting": 1, + "killed": 2, + "excepted": 2, + "finished": 2, + } + self.bar = IntProgress( # pylint: disable=blacklisted-name + value=0, + min=0, + max=2, + step=1, + description='Progress:', + bar_style='warning', # 'success', 'info', 'warning', 'danger' or '' + orientation='horizontal', + layout=Layout(width="800px")) + self.state = HTML(description="Calculation state:", value='Created', style={'description_width': 'initial'}) + children = [self.bar, self.state] + super(ProgressBarWidget, self).__init__(children=children, **kwargs) + + def update(self): + """Update the bar.""" + self.bar.value = self.correspondance[self.current_state] + if self.current_state == 'finished': + self.bar.bar_style = 'success' + elif self.current_state in ["killed", "excepted"]: + self.bar.bar_style = 'danger' + else: + self.bar.bar_style = 'info' + self.state.value = self.current_state.capitalize() + + @property + def current_state(self): + return self.process.process_state.value + + +class RunningCalcJobOutputWidget(Textarea): + """Output of a currently running calculation or one of work chain's running child.""" + + def __init__(self, process, **kwargs): + self.main_process = process + default_params = { + "value": "", + "placeholder": "Calculation output will appear here", + "description": "Calculation output:", + "layout": { + 'width': "900px", + 'height': '300px' + }, + "disabled": False, + "style": { + 'description_width': 'initial' + } + } + default_params.update(kwargs) + self.previous_calc_id = 0 + self.output = [] + super(RunningCalcJobOutputWidget, self).__init__(**default_params) + + def update(self): + """Update the displayed output.""" + calcs = get_running_calcs(self.main_process) + if calcs: + for calc in calcs: + if calc.id == self.previous_calc_id: + break + else: + self.output = [] + self.previous_calc_id = calc.id + self.value = '' + if 'remote_folder' in calc.outputs: + f_path = os.path.join(calc.outputs.remote_folder.get_remote_path(), calc.attributes['output_filename']) + if os.path.exists(f_path): + with open(f_path) as fobj: + self.output += fobj.readlines()[len(self.output):-1] + self.value = ''.join(self.output) diff --git a/aiidalab_widgets_base/search_qb.py b/aiidalab_widgets_base/search_qb.py deleted file mode 100644 index 94c70b57b..000000000 --- a/aiidalab_widgets_base/search_qb.py +++ /dev/null @@ -1,8 +0,0 @@ -from aiida.orm.querybuilder import QueryBuilder -from aiida.orm.data.structure import StructureData - -def formula_in_qb(formula): - # search for existing structures - qb = QueryBuilder() - qb.append(StructureData, filters={'extras.formula':formula}) - return [[n[0].get_ase(),n[0].pk] for n in qb.iterall()] diff --git a/aiidalab_widgets_base/structures.py b/aiidalab_widgets_base/structures.py index fe80fa2df..b8f7b7b17 100644 --- a/aiidalab_widgets_base/structures.py +++ b/aiidalab_widgets_base/structures.py @@ -1,17 +1,17 @@ -from __future__ import print_function -from __future__ import absolute_import +"""Module to provide functionality to import structures.""" import os -import ase.io -import ipywidgets as ipw -from fileupload import FileUploadWidget import tempfile -import nglview -from six.moves import zip +import datetime +from collections import OrderedDict from traitlets import Bool +import ipywidgets as ipw +from aiida.orm import CalcFunctionNode, CalcJobNode, Node, QueryBuilder, WorkChainNode, StructureData +from .utils import get_ase_from_file -class StructureUploadWidget(ipw.VBox): + +class StructureManagerWidget(ipw.VBox): # pylint: disable=too-many-instance-attributes '''Upload a structure and store it in AiiDA database. Useful class members: @@ -20,15 +20,14 @@ class StructureUploadWidget(ipw.VBox): :ivar frozen: whenter the widget is frozen (can't be modified) or not :vartype frozen: bool :ivar structure_node: link to AiiDA structure object - :vartype structure_node: StructureData or CifData - ''' + :vartype structure_node: StructureData or CifData''' + has_structure = Bool(False) frozen = Bool(False) DATA_FORMATS = ('StructureData', 'CifData') - def __init__(self, text="Upload Structure", storable=True, node_class=None, examples=[], data_importers=[], **kwargs): + + def __init__(self, importers, storable=True, node_class=None, **kwargs): """ - :param text: Text to display before upload button - :type text: str :param storable: Whether to provide Store button (together with Store format) :type storable: bool :param node_class: AiiDA node class for storing the structure. @@ -36,102 +35,63 @@ def __init__(self, text="Upload Structure", storable=True, node_class=None, exam Note: If your workflows require a specific node class, better fix it here. :param examples: list of tuples each containing a name and a path to an example structure :type examples: list - :param data_importers: list of tuples each containing a name and an object for data importing. Each object + :param importers: list of tuples each containing a name and an object for data importing. Each object should containt an empty `on_structure_selection()` method that has two parameters: structure_ase, name - :type examples: list + :type examples: list""" - """ - self._structure_sources_tab = ipw.Tab() - - self.file_upload = FileUploadWidget(text) - supported_formats = ipw.HTML("""Supported structure formats""") - - self.select_example = ipw.Dropdown(options=self.get_example_structures(examples)) - self.viewer = nglview.NGLWidget() - self.btn_store = ipw.Button( - description='Store in AiiDA', disabled=True) - self.structure_description = ipw.Text( - placeholder="Description (optional)") + from .viewers import StructureDataViewer + if not importers: # we make sure the list is not empty + raise ValueError("The parameter importers should contain a list (or tuple) of tuples " + "(\"importer name\", importer), got a falsy object.") self.structure_ase = None self._structure_node = None - self.data_format = ipw.RadioButtons( - options=self.DATA_FORMATS, description='Data type:') - structure_sources = [ipw.VBox([self.file_upload, supported_formats])] - structure_sources_names = ["Upload"] + self.viewer = StructureDataViewer(downloadable=False) - if data_importers: - for label, importer in data_importers: - structure_sources.append(importer) - structure_sources_names.append(label) - importer.on_structure_selection = self.select_structure + self.btn_store = ipw.Button(description='Store in AiiDA', disabled=True) + self.btn_store.on_click(self._on_click_store) - if examples: - structure_sources_names.append("Examples") - structure_sources.append(self.select_example) + # Description that will is stored along with the new structure. + self.structure_description = ipw.Text(placeholder="Description (optional)") - if len(structure_sources) == 1: - self._structure_sources_tab = structure_sources[0] + # Select format to store in the AiiDA database. + self.data_format = ipw.RadioButtons(options=self.DATA_FORMATS, description='Data type:') + self.data_format.observe(self.reset_structure, names=['value']) + + if len(importers) == 1: + # If there is only one importer - no need to make tabs. + self._structure_sources_tab = importers[0][1] + # Assigning a function which will be called when importer provides a structure. + importers[0][1].on_structure_selection = self.select_structure else: - self._structure_sources_tab.children = structure_sources - for i, title in enumerate(structure_sources_names): - self._structure_sources_tab.set_title(i, title) + self._structure_sources_tab = ipw.Tab() # Tabs. + self._structure_sources_tab.children = [i[1] for i in importers] # One importer per tab. + for i, (label, importer) in enumerate(importers): + # Labeling tabs. + self._structure_sources_tab.set_title(i, label) + # Assigning a function which will be called when importer provides a structure. + importer.on_structure_selection = self.select_structure if storable: if node_class is None: store = [self.btn_store, self.data_format, self.structure_description] elif node_class not in self.DATA_FORMATS: - raise ValueError("Unknown data format '{}'. Options: {}".format( - node_class, self.DATA_FORMATS)) + raise ValueError("Unknown data format '{}'. Options: {}".format(node_class, self.DATA_FORMATS)) else: self.data_format.value = node_class store = [self.btn_store, self.structure_description] else: store = [self.structure_description] store = ipw.HBox(store) - children = [self._structure_sources_tab, self.viewer, store] - super(StructureUploadWidget, self).__init__( - children=children, **kwargs) - - self.file_upload.observe(self._on_file_upload, names='data') - self.select_example.observe(self._on_select_example, names=['value']) - self.btn_store.on_click(self._on_click_store) - self.data_format.observe(self.reset_structure, names=['value']) - self.structure_description.observe(self.reset_structure, names=['value']) - - @staticmethod - def get_example_structures(examples): - if type(examples) != list: - raise ValueError("parameter examples should be of type list, {} given".format(type(examples))) - if examples: - options =[("Select structure", False)] - options += examples - return options - else: - return [] - - # pylint: disable=unused-argument - def _on_file_upload(self, change): - self.file_path = os.path.join(tempfile.mkdtemp(), self.file_upload.filename) - with open(self.file_path, 'w') as f: - f.write(self.file_upload.data) - structure_ase = self.get_ase(self.file_path) - self.select_structure(structure_ase=structure_ase, name=self.file_upload.filename) + super().__init__(children=[self._structure_sources_tab, self.viewer, store], **kwargs) - def _on_select_example(self, change): - if self.select_example.value: - structure_ase = self.get_ase(self.select_example.value) - self.file_path = self.select_example.value - else: - structure_ase = False - self.select_structure(structure_ase=structure_ase, name=self.select_example.label) - - def reset_structure(self, change=None): + def reset_structure(self, change=None): # pylint: disable=unused-argument if self.frozen: return self._structure_node = None + self.viewer.structure = None def select_structure(self, structure_ase, name): """Select structure @@ -139,65 +99,29 @@ def select_structure(self, structure_ase, name): :param structure_ase: ASE object containing structure :type structure_ase: ASE Atoms :param name: File name with extension but without path - :type name: str - """ + :type name: str""" + if self.frozen: return - self.name = name self._structure_node = None if not structure_ase: self.btn_store.disabled = True self.has_structure = False self.structure_ase = None self.structure_description.value = '' - self.reset_structure() - self.refresh_view() return self.btn_store.disabled = False self.has_structure = True - self.structure_description.value = self.get_description(structure_ase, name) + self.structure_description.value = "{} ({})".format(structure_ase.get_chemical_formula(), name) self.structure_ase = structure_ase - self.refresh_view() + self.viewer.structure = structure_ase - def get_ase(self, fname): - try: - traj = ase.io.read(fname, index=":") - except Exception as exc: - if exc.args: - print(' '.join([str(c) for c in exc.args])) - else: - print("Unknown error") - return False - if not traj: - print("Could not read any information from the file {}".format(fname)) - return False - if len(traj) > 1: - print( - "Warning: Uploaded file {} contained more than one structure. I take the first one." - .format(fname)) - return traj[0] - - def get_description(self, structure_ase, name): - formula = structure_ase.get_chemical_formula() - return "{} ({})".format(formula, name) - - def refresh_view(self): - viewer = self.viewer - # Note: viewer.clear() only removes the 1st component - # pylint: disable=protected-access - for comp_id in viewer._ngl_component_ids: - viewer.remove_component(comp_id) - if self.structure_ase is None: - return - viewer.add_component(nglview.ASEStructure(self.structure_ase)) # adds ball+stick - viewer.add_unitcell() - - # pylint: disable=unused-argument - def _on_click_store(self, change): + def _on_click_store(self, change): # pylint: disable=unused-argument self.store_structure() def store_structure(self, label=None, description=None): + """Stores the structure in AiiDA database.""" if self.frozen: return if self.structure_node is None: @@ -218,9 +142,7 @@ def freeze(self): self.frozen = True self.btn_store.disabled = True self.structure_description.disabled = True - self.file_upload.disabled = True self.data_format.disabled = True - self.select_example.disabled = True @property def node_class(self): @@ -234,29 +156,274 @@ def node_class(self, value): @property def structure_node(self): + """Returns AiiDA StructureData node.""" if self._structure_node is None: if self.structure_ase is None: return None - # determine data source - if self.name.endswith('.cif'): - source_format = 'CIF' - else: - source_format = 'ASE' # perform conversion if self.data_format.value == 'CifData': - if source_format == 'CIF': - from aiida.orm.data.cif import CifData - self._structure_node = CifData( - file=os.path.abspath(self.file_path), - scan_type='flex', - parse_policy='lazy') - else: - from aiida.orm.data.cif import CifData - self._structure_node = CifData() - self._structure_node.set_ase(self.structure_ase) + from aiida.orm.nodes.data.cif import CifData + self._structure_node = CifData() + self._structure_node.set_ase(self.structure_ase) else: # Target format is StructureData - from aiida.orm.data.structure import StructureData self._structure_node = StructureData(ase=self.structure_ase) self._structure_node.description = self.structure_description.value - self._structure_node.label = os.path.splitext(self.name)[0] - return self._structure_node \ No newline at end of file + self._structure_node.label = self.structure_ase.get_chemical_formula() + return self._structure_node + + +class StructureUploadWidget(ipw.VBox): + """Class that allows to upload structures from user's computer.""" + + def __init__(self, text="Upload Structure"): + from fileupload import FileUploadWidget + + self.on_structure_selection = lambda structure_ase, name: None + self.file_path = None + self.file_upload = FileUploadWidget(text) + supported_formats = ipw.HTML( + """ + Supported structure formats + """) + self.file_upload.observe(self._on_file_upload, names='data') + super().__init__(children=[self.file_upload, supported_formats]) + + def _on_file_upload(self, change): # pylint: disable=unused-argument + """When file upload button is pressed.""" + self.file_path = os.path.join(tempfile.mkdtemp(), self.file_upload.filename) + with open(self.file_path, 'w') as fobj: + fobj.write(self.file_upload.data.decode("utf-8")) + structure_ase = get_ase_from_file(self.file_path) + self.on_structure_selection(structure_ase=structure_ase, name=self.file_upload.filename) + + +class StructureExamplesWidget(ipw.VBox): + """Class to provide example structures for selection.""" + + def __init__(self, examples, **kwargs): + self.on_structure_selection = lambda structure_ase, name: None + self._select_structure = ipw.Dropdown(options=self.get_example_structures(examples)) + self._select_structure.observe(self._on_select_structure, names=['value']) + super().__init__(children=[self._select_structure], **kwargs) + + @staticmethod + def get_example_structures(examples): + """Get the list of example structures.""" + if not isinstance(examples, list): + raise ValueError("parameter examples should be of type list, {} given".format(type(examples))) + return [("Select structure", False)] + examples + + def _on_select_structure(self, change): # pylint: disable=unused-argument + """When structure is selected.""" + if not self._select_structure.value: + return + structure_ase = get_ase_from_file(self._select_structure.value) + self.on_structure_selection(structure_ase=structure_ase, name=self._select_structure.label) + + +class StructureBrowserWidget(ipw.VBox): + """Class to query for structures stored in the AiiDA database.""" + + def __init__(self): + # Find all process labels + qbuilder = QueryBuilder() + qbuilder.append(WorkChainNode, project="label") + qbuilder.order_by({WorkChainNode: {'ctime': 'desc'}}) + process_labels = {i[0] for i in qbuilder.all() if i[0]} + + layout = ipw.Layout(width="900px") + self.mode = ipw.RadioButtons(options=['all', 'uploaded', 'edited', 'calculated'], + layout=ipw.Layout(width="25%")) + + # Date range + self.dt_now = datetime.datetime.now() + self.dt_end = self.dt_now - datetime.timedelta(days=10) + self.date_start = ipw.Text(value='', description='From: ', style={'description_width': '120px'}) + + self.date_end = ipw.Text(value='', description='To: ') + self.date_text = ipw.HTML(value='
Select the date range:
') + self.btn_date = ipw.Button(description='Search', layout={'margin': '1em 0 0 0'}) + self.age_selection = ipw.VBox( + [self.date_text, ipw.HBox([self.date_start, self.date_end]), self.btn_date], + layout={ + 'border': '1px solid #fafafa', + 'padding': '1em' + }) + + # Labels + self.drop_label = ipw.Dropdown(options=({'All'}.union(process_labels)), + value='All', + description='Process Label', + style={'description_width': '120px'}, + layout={'width': '50%'}) + + self.btn_date.on_click(self.search) + self.mode.observe(self.search, names='value') + self.drop_label.observe(self.search, names='value') + + h_line = ipw.HTML('AiiDA lab widgets | @@ -15,7 +16,7 @@
---|