Commit 8a606d47 authored by Nigel Palmer's avatar Nigel Palmer
Browse files

Added content for the Flare Python 2.0 release.

parents
*.flr binary
*.py text
*.pdb text
*.sdf text
*.crd text
*.top text
*.mol text
*.mol2 text
*.smi text
__pycache__/
*.py[cod]
.pytest_cache
desktop.ini
# Changelog
## [2.0] - 2018-07-25
### Added
- Example on how to create a GUI using QtDesigner (qtdesignerexample)
- Example on how to add controls to the ribbon (ribbon)
- Example on how to add context menu items (contextmenus.py)
- Example on how to download and add proteins (download1oit.py)
- Example on how to update ligand properties using a rest service (restexample.py, runrestservice.py)
Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
\ No newline at end of file
# Flare Python Developer
Welcome to the [Flare](https://www.cresset-group.com/flare/) Python Developer repository. This repository contains a collection of free Python scripts which demonstrate how to write Flare (TM) extensions. After installing these extensions a new ribbon tab titled "Developer" will be added to the Flare GUI.
If you are not a developer and are looking for Flare extensions, please visit the [flare-python-extensions repository](https://gitlab.com/cresset/flare-python-extensions). For scripts which allows Flare functionality to be accessed from the command line, please visit the [flare-python-pyflare repository](https://gitlab.com/cresset/flare-python-pyflare).
The developer example extensions are provided to you on an "AS-IS" basis, however if you need help with these extensions or have suggestions of your own please contact [Cresset support](https://www.cresset-group.com/about-us/contact-us/).
## Installing
See the [installation guide](https://gitlab.com/cresset/flare-python-extensions/README.md).
## Extensions Descriptions
### Context Menus (contextmenus.py)
Demonstrates how to add items to context menus.
### Download 1oit (download1oit.py)
Demonstrates how to download a protein from a remote server.
**New Ribbon Controls:**
*Developer -> Download -> 1oit* - Download the protein '1oit' from the RCSB.
### Qt Designer Example (qtdesignerexample)
Demonstrates how to show a dialog created in Qt Designer.
In this example we load the file 'timerdialog.ui' which was created in Qt Designer and display it in a dialog.
**New Ribbon Controls:**
*Developer -> Developer -> Qt Designer Example* - Open the dialog created in Qt Designer.
### Rest Example (restexample.py)
Demonstrates how to use a rest server to add properties to Ligand table when ligands are added.
**Callbacks:**
*flare.callbacks.ligands_added* - When a ligand is added background threads are started which calculates a property. The background thread then ask the main thread to add a property to the ligand. This example by default generates a value on the background thread instead of using a REST server. To use a REST server modify the the _calculate_data() method.
### Ribbon (ribbon)
Demonstrates how to create a ribbon tab and group containing various buttons and widgets.
## Scripts Descriptions
### Run REST Service (runrestservice.py)
A REST service which returns the atom count of a SDF file.
The service runs on localhost:8080. When a get request is received with a data payload of
a SDF file a response is sent containing the atom count.
This script can be used to test the restexample.py extension.
Flask needs to be installed to run this script:
> pyflare -m pip install flask flask_restful
To run this script:
> pyflare runrestservice.py
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Demonstrates how to add items to context menus."""
from PySide2 import QtCore, QtWidgets
from cresset import flare
from cressetdeveloperutils import report_error
@flare.extension
class ContextMenusExtension:
"""Add menu items to various context menus."""
def load(self):
"""Load the extension."""
flare.callbacks.atom_context_menu.add(self._atom_menu)
flare.callbacks.ligand_context_menu.add(self._ligand_menu)
flare.callbacks.pose_context_menu.add(self._pose_menu)
flare.callbacks.role_context_menu.add(self._role_menu)
flare.callbacks.ligand_table_header_context_menu.add(self._ligand_table_header_menu)
flare.callbacks.protein_residue_context_menu.add(self._protein_residue_menu)
flare.callbacks.protein_sequence_context_menu.add(self._protein_sequence_menu)
flare.callbacks.protein_context_menu.add(self._protein_menu)
print(f"Loaded {self.__class__.__module__}.{self.__class__.__name__}")
def _atom_menu(self, menu, atoms):
picked_atoms = flare.main_window().picked_atoms
action = menu.addAction("Atom Python Menu")
action.triggered.connect(
report_error(
lambda: self._show_dialog(
f"{len(atoms)} current atoms.\n{len(picked_atoms)} picked atoms."
)
)
)
def _ligand_menu(self, menu, ligand):
action = menu.addAction("Ligand Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Opened on '{ligand.title}'."))
)
def _pose_menu(self, menu, pose):
action = menu.addAction("Pose Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Pose SMILES '{pose.smiles()}'."))
)
def _role_menu(self, menu, role):
action = menu.addAction("Role Python Menu")
action.triggered.connect(report_error(lambda: self._show_dialog(f"Role '{role.name}'.")))
def _ligand_table_header_menu(self, menu, header):
action = menu.addAction("Header Python Menu")
action.triggered.connect(report_error(lambda: self._show_dialog(f"Header '{header}'.")))
def _protein_residue_menu(self, menu, residue):
action = menu.addAction("Residue Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Residue '{str(residue)}'."))
)
def _protein_sequence_menu(self, menu, sequence):
action = menu.addAction("Sequence Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Sequence '{sequence.chain} {sequence.name}'."))
)
def _protein_menu(self, menu, protein):
action = menu.addAction("Protein Python Menu")
action.triggered.connect(
report_error(lambda: self._show_dialog(f"Protein '{protein.title}'."))
)
@staticmethod
def _show_dialog(text):
"""Show the dialog with the text."""
main_window = flare.main_window()
dialog = QtWidgets.QDialog(main_window.widget())
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
layout = QtWidgets.QVBoxLayout(dialog)
layout.addWidget(QtWidgets.QLabel(text, dialog))
dialog.show()
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Provides utility functions used by other scripts."""
import traceback
from cresset import flare
from PySide2 import QtCore, QtWidgets
def report_error(func):
"""Return a function which calls `func` and shows an error dialog if there an error."""
# noqa: D202
def show_error_dialog_on_exception(*args, **kw):
"""Show an error dialog if `func` raises an exception."""
try:
func(*args, **kw)
except Exception as e:
parent = flare.main_window().widget()
box = QtWidgets.QMessageBox(parent)
box.setAttribute(QtCore.Qt.WA_DeleteOnClose)
box.setIcon(QtWidgets.QMessageBox.Critical)
box.setDetailedText(traceback.format_exc())
box.setText(str(e))
box.exec_()
raise # Raise the error so its reported in the python log
return show_error_dialog_on_exception
# Using the PyDev Eclipse IDE for with Flare
[PyDev](http://www.pydev.org/index.html) is a Python IDE for [Eclipse](http://www.eclipse.org/) which can be use to run Flare Python scripts or remotely debug Flare extensions.
## Installation
1. Install Eclipse from http://www.eclipse.org/downloads/
2. Install PyDev for Eclipse using the instructions at http://www.pydev.org/manual_101_install.html.
3. Install PyDev for Flare by running:
> pyflare -m pip install --user pydevd
## Configuring the interpreter
1. In Eclipse select Window > Preferences > PyDev > Interpreters >Python Interpreter
2. Press "New...", set the name to pyflare and the executable to the "pyflare" executable located in the Flare installation directory. Press 'OK'.
3. Keep the defaults when asked to configure the 'pythonpath' and press 'OK'.
4. PyDev is now configured to use the pyflare Python Interpreter.
![](images/ConfigureEclipsePythonInterpreter.png)
## Running Scripts
1. In Eclipse select File > New > PyDev Project.
![](images/EclipseNewProjectWizard.png)
2. Enter a project name and set the interpreter to 'pyflare'. Press 'Finish' to create the project.
3. Select File > New > PyDev Module.
4. Give the module a name and press 'Finish'.
![](images/EclipseNewModule.png)
5. Select the 'Module: Main' template and press 'Ok'.
![](images/EclipseNewModuleSelectTemplate.png)
6. Now you have a Python module which you can start writing your script from.
Below is an example script which downloads a protein and prints its residues.
Copy the script into Eclipse and select File > Run to run it.
```python
import urllib.request
from cresset import flare
if __name__ == "__main__":
project = flare.Project()
url = "http://files.rcsb.org/view/1oit.pdb"
response = urllib.request.urlopen(url)
text = response.read().decode("utf-8")
project.proteins.extend(flare.read_string(text, "pdb"))
for residue in project.proteins[0].residues:
print(residue)
```
![](images/EclipseRunningPyFlareScript.png)
## Remote Debugging Flare Extensions
1. Open the extension to debug in Eclipse by selection File > Open File. For example open the importexport.py extension.
2. Add a breaking point to a extension by inserting the line 'import pydevd; pydevd.settrace()'.
For example add the line to the start of the ImportExportExtension._export_ligand_table function.
![](images/EclipseSettrace.png)
3. In Eclipse switch to the Debug perspective via Window > Perspective > Open Perspective > Other > Debug.
4. Start the remote debugger server by selecting Pydev > Start Debug Server.
![](images/EclipseStartDebugServer.png)
5. Start Flare and and wait for it to run the 'import pydevd; pydevd.settrace()'. For this example the
'import pydevd; pydevd.settrace()' is only called when the 'Export Ligands Table' button on the 'Extensions' tab
is pressed.
6. Eclipse should now have hit the 'import pydevd; pydevd.settrace()' break point.
![](images/EclipseDebuggingExtension.png)
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Demonstrates how to download a protein from a remote server.
Ribbon Controls:
Developer -> Download -> 1oit
Download the protein '1oit' from the RCSB.
"""
import urllib.request
from cresset import flare
from cressetdeveloperutils import report_error
@flare.extension
class Download1oitExtension:
"""Add a button which downloads 1oit."""
def load(self):
"""Load the extension."""
group = flare.main_window().ribbon["Developer"]["Download"]
control = group.add_button("1oit", report_error(self._download_1oit))
control.tooltip = "Download the protein '1oit' from the RCSB."
print(f"Loaded {self.__class__.__module__}.{self.__class__.__name__}")
@staticmethod
def _download_1oit():
"""Download 1oit and add it to the protein role."""
url = "http://files.rcsb.org/view/1oit.pdb"
response = urllib.request.urlopen(url)
text = response.read().decode("utf-8")
project = flare.main_window().project
project.proteins.extend(flare.read_string(text, "pdb"))
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Demonstrates how to show a dialog created in Qt Designer.
In this example we load the file 'timerdialog.ui' which was created in Qt Designer
and display it in a dialog.
Ribbon Controls:
Developer -> Developer -> Qt Designer Example
Open the dialog created in Qt Designer.
"""
import os
import threading
import time
import datetime
from PySide2 import QtCore, QtWidgets, QtUiTools
from cresset import flare
from cressetdeveloperutils import report_error
UI_FILE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "timerdialog.ui")
class TimerDialog(QtWidgets.QDialog):
"""Dialog which displays a form created in Qt Designer."""
def __init__(self, parent=None):
"""Create the dialog."""
super().__init__(parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self._thread = None
self._stopping = False
self._start_time = datetime.datetime.now()
# Load the Qt Designer form
file = QtCore.QFile(UI_FILE_PATH)
file.open(QtCore.QFile.ReadOnly)
loader = QtUiTools.QUiLoader()
self._widget = loader.load(file, parent)
# Add the form to this dialog
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._widget)
# Connect the signals from buttons in the .ui form to functions in this class
self._widget.start_button.pressed.connect(self._on_start_stop)
self._widget.reset_button.pressed.connect(self._on_reset)
def __del__(self):
"""Stop the running background thread."""
if self._thread:
self._stopping = True
self._thread.join()
def _on_start_stop(self):
"""Start or stop the timer."""
if self._thread:
# Stop the timer
self._stopping = True
self._thread.join()
self._thread = None
self._widget.reset_button.setEnabled(True)
self._widget.start_button.setText("Start")
else:
# Start the timer
# A background thread is created which updates the label in the .ui form
self._stopping = False
self._thread = threading.Thread(target=self._run)
self._thread.start()
self._widget.reset_button.setEnabled(False)
self._widget.start_button.setText("Stop")
def _on_reset(self):
"""Reset the timer to 0."""
self._start_time = datetime.datetime.now()
self._widget.time_label.setText("0")
def _run(self):
"""Periodically updates the timer."""
while not self._stopping:
time_diff = datetime.datetime.now() - self._start_time
# All UI updates must take place on the main thread
# So _on_update_label is called on the main thread
# with the value to display
flare.invoke_later(self._on_update_label, args=(time_diff,))
time.sleep(0.01)
def _on_update_label(self, time_diff):
"""Set the value in the .ui form label to time_diff."""
self._widget.time_label.setText(str(time_diff))
@flare.extension
class QtDesignerExampleExtension:
"""Create a ribbon tab containing a button to show the timer dialog."""
def load(self):
"""Add a buttons to the ribbon."""
ribbon = flare.main_window().ribbon["Developer"]["Qt Designer Example"]
control = ribbon.add_button("Timer", report_error(self._show_timer_dialog))
control.tooltip = "Open the dialog which was created in Qt Designer."
@staticmethod
def _show_timer_dialog():
"""Show the timer dialog."""
dialog = TimerDialog(flare.main_window().widget())
dialog.show()
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>271</width>
<height>200</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>200</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="start_button">
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="reset_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Reset</string>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="time_label">
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Cresset Biomolecular Discovery Ltd.
# Released under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/).
# Originally downloaded from https://gitlab.com/cresset
"""Demonstrates how to use a rest server to add properties to Ligand table when ligands are added.
Callbacks:
flare.callbacks.ligands_added
When a ligand is added background threads are started which calculates a property.
The background thread then ask the main thread to add a property to the ligand.
This example by default generates a value on the background thread instead of using a REST
server. To use a REST server modify the the _calculate_data() method.
"""
import threading
import time
import random
import requests
import traceback
from cresset import flare
@flare.extension
class RestExampleExtension:
"""Calculate properties on ligands when they are added to a project."""
def load(self):
"""Start listening for molecules being added."""
flare.callbacks.ligands_added.add(self._on_ligands_added)
print(f"Loaded {self.__class__.__module__}.{self.__class__.__name__}")
def _on_ligands_added(self, ligands):
"""Start a background thread which will calculate the property."""
# Convert the ligand to SDF format and send it to the background thread
for ligand in ligands:
sdf = ligand.write_string("sdf")
thread = threading.Thread(target=self._calculate_data, args=(ligand, sdf))
thread.start()
def _calculate_data(self, ligand, sdf):
"""Calculate the property, then inform the main thread to update the ligand."""
# In this example we generate a random number instead of getting the data from a REST
# service so that the unmodified version of this script does not send data to external
# resources.
#
# Uncomment _data_for_ligand_rest and comment out _data_for_ligand_random and this
# extension will instead send the SDF file to a rest server running on localhost.
# See the script developer-examples-pyflare-scripts/runrestservice.py for an example
# REST server which will work with _data_for_ligand_rest.
data = self._data_for_ligand_random(sdf)
# data = self._data_for_ligand_rest(sdf)
# Send the calculated data back to the main thread so the ligand can be updated
flare.invoke_later(target=self._update_ligands, args=(data, ligand))
def _data_for_ligand_random(self, sdf):
"""Return a random number and wait for some time to simulate a request to a REST server."""
# In this example a random number is generated but for real applications we could
# have sent the molecule off to a REST server and waited for a response. As this
# function is being called on a background thread Flare can still be used
# while waiting for the REST server.
time.sleep(0.1)
data = random.random()
return round(data, 2)
def _data_for_ligand_rest(self, sdf):
"""Send the `sdf` file to the server and return the response.
See the developer-examples-pyflare-scripts/runrestservice.py script for
an example of a REST service.
"""
data = None