[pytango] 172/483: api2 initial implementation
Sandor Bodo-Merle
sbodomerle-guest at moszumanska.debian.org
Thu Sep 28 19:14:37 UTC 2017
This is an automated email from the git hooks/post-receive script.
sbodomerle-guest pushed a commit to annotated tag bliss_8.10
in repository pytango.
commit 9d32c90b235a59e3312f5b8e4fd43e63c117da23
Author: tiagocoutinho <tiagocoutinho at 4e9c00fd-8f2e-0410-aa12-93ce3db5e235>
Date: Mon Oct 22 15:42:03 2012 +0000
api2 initial implementation
git-svn-id: http://svn.code.sf.net/p/tango-cs/code/bindings/PyTango/trunk@21371 4e9c00fd-8f2e-0410-aa12-93ce3db5e235
---
PyTango/api2.py | 185 +++++++++++++++--------
doc/tep/tep-0001.rst | 405 ++++++++++++++++++++++++++++++++++++---------------
2 files changed, 410 insertions(+), 180 deletions(-)
diff --git a/PyTango/api2.py b/PyTango/api2.py
index b46a32a..c6ca9b5 100644
--- a/PyTango/api2.py
+++ b/PyTango/api2.py
@@ -21,7 +21,9 @@
##
################################################################################
-"""This module provides a high level device server API. It implements
+""".. _pytango-api2:
+
+This module provides a high level device server API. It implements
:ref:`TEP1 <pytango-TEP1>`. It exposes an easier API for developing a tango
device server.
@@ -57,11 +59,18 @@ on the low level :mod:`PyTango` server API:
#. read attribute methods can set attribute return value with::
- self.<attr name> = <value>
+ def read_voltage(self):
+ return value
+
+ # or
+
+ def read_voltage(self):
+ self.voltage = value
instead of::
- attr.set_value(<value>)
+ def read_voltage(self, attr):
+ attr.set_value(value)
:class:`Device` works very well in conjuction with:
@@ -99,10 +108,10 @@ Here is an example of a PowerSupply device with:
host = device_property()
def read_voltage(self):
- self.voltage = 10.0
+ return 10.0
def read_current(self):
- self.current = 2.5, time(), AttrQuality.ON
+ return 2.5, time(), AttrQuality.ON
@DebugIt()
def write_current(self):
@@ -207,7 +216,6 @@ from __future__ import print_function
__all__ = ["DeviceMeta", "Device", "LatestDeviceImpl", "attribute", "command",
"device_property", "class_property"]
-import inspect
import functools
import __builtin__
@@ -313,7 +321,57 @@ def get_tango_type(dtype):
get_tango_type.__doc__ = __type_doc
-def check_tango_device_klass_attribute_method(tango_device_klass, method_name):
+def set_complex_value(attr, value):
+ is_tuple = isinstance(value, tuple)
+ dtype, fmt = attr.get_data_type(), attr.get_data_format()
+ if dtype == CmdArgType.DevEncoded:
+ if is_tuple and len(value) == 4:
+ attr.set_value_date_quality(*value)
+ elif is_tuple and len(value) == 3 and is_non_str_seq(value[0]):
+ attr.set_value_date_quality(value[0][0], value[0][1], *value[1:])
+ else:
+ attr.set_value(*value)
+ else:
+ if is_tuple:
+ if len(value) == 3:
+ if fmt == AttrDataFormat.SCALAR:
+ attr.set_value_date_quality(*value)
+ elif fmt == AttrDataFormat.SPECTRUM:
+ if is_seq(value[0]):
+ attr.set_value_date_quality(*value)
+ else:
+ attr.set_value(value)
+ else:
+ if is_seq(value[0]) and is_seq(value[0][0]):
+ attr.set_value_date_quality(*value)
+ else:
+ attr.set_value(value)
+ else:
+ attr.set_value(value)
+ else:
+ attr.set_value(value)
+
+def check_tango_device_klass_attribute_read_method(tango_device_klass, method_name):
+ """Checks if method given by it's name for the given DeviceImpl class has
+ the correct signature. If a read/write method doesn't have a parameter
+ (the traditional Attribute), then the method is wrapped into another method
+ which has correct parameter definition to make it work.
+
+ :param tango_device_klass: a DeviceImpl class
+ :type tango_device_klass: class
+ :param method_name: method to be cheched
+ :type attr_data: str"""
+ read_method = getattr(tango_device_klass, method_name)
+
+ @functools.wraps(read_method)
+ def read_attr(self, attr):
+ ret = read_method(self)
+ if not attr.get_value_flag() and ret is not None:
+ set_complex_value(attr, ret)
+ return ret
+ setattr(tango_device_klass, method_name, read_attr)
+
+def check_tango_device_klass_attribute_write_method(tango_device_klass, method_name):
"""Checks if method given by it's name for the given DeviceImpl class has
the correct signature. If a read/write method doesn't have a parameter
(the traditional Attribute), then the method is wrapped into another method
@@ -323,24 +381,13 @@ def check_tango_device_klass_attribute_method(tango_device_klass, method_name):
:type tango_device_klass: class
:param method_name: method to be cheched
:type attr_data: str"""
- f_obj = real_f_obj = getattr(tango_device_klass, method_name)
-
- # discover the real method because it may be hidden by a tango decorator
- # like PyTango.DebugIt. Unfortunately we cannot detect other decorators yet
- while hasattr(real_f_obj, "_wrapped"):
- real_f_obj = real_f_obj._wrapped
- argspec = inspect.getargspec(real_f_obj)
- if argspec.varargs and len(argspec.varargs):
- return
- nb = len(argspec.args) - 1
- if argspec.defaults:
- nb -= len(argspec.defaults)
- if nb > 0:
- return
- @functools.wraps(f_obj)
- def f_attr(self, attr):
- return f_obj(self)
- setattr(tango_device_klass, method_name, f_attr)
+ write_method = real_f_obj = getattr(tango_device_klass, method_name)
+
+ @functools.wraps(write_method)
+ def write_attr(self, attr):
+ value = attr.get_write_value()
+ return write_method(self, value)
+ setattr(tango_device_klass, method_name, write_attr)
def check_tango_device_klass_attribute_methods(tango_device_klass, attr_data):
"""Checks if the read and write methods have the correct signature. If a
@@ -352,9 +399,9 @@ def check_tango_device_klass_attribute_methods(tango_device_klass, attr_data):
:param attr_data: the attribute data information
:type attr_data: AttrData"""
if attr_data.attr_write in (AttrWriteType.READ, AttrWriteType.READ_WRITE):
- check_tango_device_klass_attribute_method(tango_device_klass, attr_data.read_method_name)
+ check_tango_device_klass_attribute_read_method(tango_device_klass, attr_data.read_method_name)
if attr_data.attr_write in (AttrWriteType.WRITE, AttrWriteType.READ_WRITE):
- check_tango_device_klass_attribute_method(tango_device_klass, attr_data.write_method_name)
+ check_tango_device_klass_attribute_write_method(tango_device_klass, attr_data.write_method_name)
def create_tango_deviceclass_klass(tango_device_klass, attrs=None):
klass_name = tango_device_klass.__name__
@@ -375,9 +422,15 @@ def create_tango_deviceclass_klass(tango_device_klass, attrs=None):
device_property_list = {}
cmd_list = {}
devclass_name = klass_name + "Class"
+
+ def device_class_constructor(self, name):
+ DeviceClass.__init__(self, name)
+ self.set_type(name)
+
devclass_attrs = dict(class_property_list=class_property_list,
device_property_list=device_property_list,
cmd_list=cmd_list, attr_list=attr_list)
+ devclass_attrs['__init__'] = device_class_constructor
return type(devclass_name, (DeviceClass,), devclass_attrs)
def init_tango_device_klass(tango_device_klass, attrs=None, tango_class_name=None):
@@ -453,35 +506,8 @@ class AttrData2(AttrData):
return self.get_attribute(obj)
def __set__(self, obj, value):
- is_tuple = isinstance(value, tuple)
attr = self.get_attribute(obj)
- dtype, fmt = attr.get_data_type(), attr.get_data_format()
- if dtype == CmdArgType.DevEncoded:
- if is_tuple and len(value) == 4:
- attr.set_value_date_quality(*value)
- elif is_tuple and len(value) == 3 and is_non_str_seq(value[0]):
- attr.set_value_date_quality(value[0][0], value[0][1], *value[1:])
- else:
- attr.set_value(*value)
- else:
- if is_tuple:
- if len(value) == 3:
- if fmt == AttrDataFormat.SCALAR:
- attr.set_value_date_quality(*value)
- elif fmt == AttrDataFormat.SPECTRUM:
- if is_seq(value[0]):
- attr.set_value_date_quality(*value)
- else:
- attr.set_value(value)
- else:
- if is_seq(value[0]) and is_seq(value[0][0]):
- attr.set_value_date_quality(*value)
- else:
- attr.set_value(value)
- else:
- attr.set_value(value)
- else:
- attr.set_value(value)
+ set_complex_value(attr, value)
def __delete__(self, obj):
obj.remove_attribute(self.attr_name)
@@ -494,8 +520,9 @@ def attribute(**kwargs):
attribute.__doc__ = """\
declares a new tango attribute in a :class:`Device`. To be used like the python
-native :obj:`property` function. To declare a scalar, PyTango.DevDouble, read-only
-attribute called *voltage* in a *PowerSupply* :class:`Device` do::
+native :obj:`property` function. For exampke, to declare a scalar,
+`PyTango.DevDouble`, read-only attribute called *voltage* in a *PowerSupply*
+:class:`Device` do::
class PowerSupply(Device):
@@ -541,9 +568,47 @@ It receives multiple keyword arguments.
:type fwrite: :obj:`str` or :obj:`callable`
:param is_allowed: is allowed method name or method object [default: 'is_<attr_name>_allowed']
:type is_allowed: :obj:`str` or :obj:`callable`
- :param label: attribute label
- :type label: obj:`str`
-
+ :param label: attribute label [default: attribute name]
+ :type label: :obj:`str`
+ :param description: attribute description [default: empty string]
+ :type description: :obj:`str`
+ :param unit: attribute unit [default: empty string]
+ :type unit: :obj:`str`
+ :param standard_unit: attribute standard unit [default: empty string]
+ :type standard_unit: :obj:`str`
+ :param display_unit: attribute display unit [default: empty string]
+ :type display_unit: :obj:`str`
+ :param format: attribute representation format [default: '6.2f']
+ :type format: :obj:`str`
+ :param min_value: attribute minimum allowed value [default: None]
+ :type min_value: :obj:`str`
+ :param max_value: attribute maximum allowed value [default: None]
+ :type max_value: :obj:`str`
+ :param min_alarm: minimum value to trigger attribute alarm [default: None]
+ :type min_alarm: :obj:`str`
+ :param max_alarm: maxmimum value to trigger attribute alarm [default: None]
+ :type max_alarm: :obj:`str`
+ :param min_warning: minimum value to trigger attribute warning [default: None]
+ :type min_warning: :obj:`str`
+ :param max_warning: attribute maxmimum value to trigger attribute warning [default: None]
+ :type max_warning: :obj:`str`
+ :param delta_val: attribute
+ :type delta_val: :obj:`str`
+ :param delta_t: attribute
+ :type delta_t: :obj:`str`
+ :param abs_change: attribute
+ :type abs_change: :obj:`str`
+ :param rel_change: attribute
+ :type rel_change: :obj:`str`
+ :param period: attribute
+ :type period: :obj:`str`
+ :param archive_abs_change: attribute
+ :type archive_abs_change: :obj:`str`
+ :param archive_rel_change: attribute
+ :type archive_rel_change: :obj:`str`
+ :param archive_period: attribute
+ :type archive_period: :obj:`str`
+
.. _pytango-api2-datatypes:
.. rubric:: Data type equivalence
diff --git a/doc/tep/tep-0001.rst b/doc/tep/tep-0001.rst
index 042eaac..c84923f 100644
--- a/doc/tep/tep-0001.rst
+++ b/doc/tep/tep-0001.rst
@@ -12,13 +12,12 @@ TEP 1 - Device Server API Level 2
================== ====================================================
Title: Device Server API Level 2
Version: 2.0.0
- Last-Modified: 18-Oct-2012
+ Last-Modified: 22-Oct-2012
Author: Tiago Coutinho <tcoutinho at cells.es>
Status: Active
Type: Standards Track
Content-Type: text/x-rst
Created: 17-Oct-2012
- Post-History: 18-Oct-2012
================== ====================================================
Abstract
@@ -31,13 +30,282 @@ Rationale
The code for Tango device servers written in Python often obey a pattern. It
would be nice if non tango experts could create tango device servers without
-having to code some obscure tango related code. Some of the code which
-is repeated over and over again in almost all python device servers could be
-hidden from the tango developer.
+having to code some obscure tango related code.
+It would also be nice if the tango programming interface would be more pythonic.
+The final goal is to make writting tango device servers as easy as::
+
+ class Motor(Device):
+ __metaclass__ = DeviceMeta
+
+ position = attribute()
+
+ def read_position(self):
+ return 2.3
+
+ @command()
+ def move(self, position):
+ pass
+
+ if __name__ == "__main__":
+ server_run((Motor,))
+
Places to simplify
===================
+After looking at most python device servers one can see some patterns:
+
+At `<Device>` class level:
+
+ #. <Device> always inherits from latest available DeviceImpl from pogo version
+ #. constructor always does the same:
+ #. calls super constructor
+ #. debug message
+ #. calls init_device
+
+ #. all methods have debug_stream as first instruction
+ #. init_device does additionaly get_device_properties()
+ #. *read attribute* methods follow the pattern::
+
+ def read_Attr(self, attr):
+ self.debug_stream()
+ value = get_value_from_hardware()
+ attr.set_value(value)
+
+ #. *write attribute* methods follow the pattern::
+
+ def write_Attr(self, attr):
+ self.debug_stream()
+ w_value = attr.get_write_value()
+ apply_value_to_hardware(w_value)
+
+At `<Device>Class` class level:
+
+ #. A <Device>Class class exists for every <DeviceName> class
+ #. The <Device>Class class only contains attributes, commands and
+ properties descriptions (no logic)
+ #. The attr_list description always follows the same (non explicit) pattern
+ (and so does cmd_list, class_property_list, device_property_list)
+ #. the syntax for attr_list, cmd_list, etc is far from understandable
+
+At `main()` level:
+
+ #. The main() method always does the same:
+ #. create `Util`
+ #. register tango class
+ #. when registering a python class to become a tango class, 99.9% of times
+ the python class name is the same as the tango class name (example:
+ Motor is registered as tango class "Motor")
+ #. call `server_init()`
+ #. call `server_run()`
+
+High level API
+===============
+
+The goals of the high level API are:
+
+Maintain all features of low-level API available from high-level API
+--------------------------------------------------------------------------------
+
+Everything that was done with the low-level API must also be possible to do
+with the new API.
+
+All tango features should be available by direct usage of the new simplified,
+cleaner high-level API and through direct access to the low-level API.
+
+Automatic inheritance from the latest** :class:`~PyTango.DeviceImpl`
+--------------------------------------------------------------------------------
+
+Currently Devices need to inherit from a direct Tango device implementation
+(:class:`~PyTango.DeviceImpl`, or :class:`~PyTango.Device_2Impl`,
+:class:`~PyTango.Device_3Impl`, :class:`~PyTango.Device_4Impl`, etc)
+according to the tango version being used during the development.
+
+In order to keep the code up to date with tango, every time a new Tango IDL
+is released, the code of **every** device server needs to be manually
+updated to ihnerit from the newest tango version.
+
+By inheriting from a new high-level :class:`~PyTango.api2.Device` (which
+itself automatically *decides* from which DeviceImpl version it should
+inherit), the device servers are always up to date with the latest tango
+release without need for manual intervention (see :mod:`PyTango.api2`).
+
+Low-level way::
+
+ class Motor(PyTango.Device_4Impl):
+ pass
+
+High-level way::
+
+ class Motor(PyTango.api2.Device):
+ pass
+
+Default implementation of :class:`~PyTango.api2.Device` constructor
+--------------------------------------------------------------------------------
+
+99% of the different device classes which inherit from low level
+:class:`~PyTango.DeviceImpl` only implement `__init__` to call their
+`init_device` (see :mod:`PyTango.api2`).
+
+:class:`~PyTango.api2.Device` already calls init_device.
+
+Low-level way::
+
+ class Motor(PyTango.Device_4Impl):
+
+ def __init__(self, dev_class, name):
+ PyTango.Device_4Impl.__init__(self, dev_class, name)
+ self.init_device()
+
+High-level way::
+
+ class Motor(PyTango.api2.Device):
+
+ # Nothing to be done!
+
+ pass
+
+Default implementation of :meth:`~PyTango.api2.Device.init_device`
+--------------------------------------------------------------------------------
+
+99% of different device classes which inherit from low level
+:class:`~PyTango.DeviceImpl` have an implementation of `init_device` which
+*at least* calls :meth:`~PyTango.DeviceImpl.get_device_properties`
+(see :mod:`PyTango.api2`).
+
+:meth:`~PyTango.api2.Device.init_device` already calls :meth:`~PyTango.api2.Device.get_device_properties`.
+
+Low-level way::
+
+ class Motor(PyTango.Device_4Impl):
+
+ def init_device(self):
+ self.get_device_properties()
+
+High-level way::
+
+ class Motor(PyTango.api2.Device):
+ # Nothing to be done!
+ pass
+
+Remove the need to code :class:`~PyTango.DeviceClass`
+--------------------------------------------------------------------------------
+
+99% of different device servers only need to implement their own subclass
+of :class:`~PyTango.DeviceClass` to register the attribute, commands,
+device and class properties by using the corresponding
+:obj:`~PyTango.DeviceClass.attr_list`, :obj:`~PyTango.DeviceClass.cmd_list`,
+:obj:`~PyTango.DeviceClass.device_property_list` and :obj:`~PyTango.DeviceClass.class_property_list`.
+
+With the high-level API we completely remove the need to code the
+:class:`~PyTango.DeviceClass` by registering attribute, commands,
+device and class properties in the :class:`~PyTango.api2.Device` with a more
+pythonic API (see :mod:`PyTango.api2`)
+
+
+#. Hide `<Device>Class` class completely
+#. simplify `main()`
+
+Low-level way::
+
+ class Motor(PyTango.Device_4Impl):
+
+ def read_Position(self, attr):
+ pass
+
+ class MotorClass(PyTango.DeviceClass):
+
+ class_property_list = { }
+ device_property_list = { }
+ cmd_list = { }
+
+ attr_list = {
+ 'Position':
+ [[PyTango.DevDouble,
+ PyTango.SCALAR,
+ PyTango.READ]],
+ }
+
+ def __init__(self, name):
+ PyTango.DeviceClass.__init__(self, name)
+ self.set_type(name)
+
+High-level way::
+
+ class Motor(PyTango.api2.Device):
+
+ position = PyTango.api2.attribute(dtype=float, )
+
+ def read_position(self):
+ pass
+
+Pythonic read/write attribute
+--------------------------------------------------------------------------------
+
+With the low level API, it feels strange for a non tango programmer to have
+to write::
+
+ def read_Position(self, attr):
+ # ...
+ attr.set_value(new_position)
+
+ def read_Position(self, attr):
+ # ...
+ attr.set_value_date_quality(new_position, time.time(), AttrQuality.CHANGING)
+
+A more pythonic away would be::
+
+ def read_position(self):
+ # ...
+ self.position = new_position
+
+ def read_position(self):
+ # ...
+ self.position = new_position, time.time(), AttrQuality.CHANGING
+
+Or even::
+
+ def read_position(self):
+ # ...
+ return new_position
+
+ def read_position(self):
+ # ...
+ return new_position, time.time(), AttrQuality.CHANGING
+
+Simplify `main()`
+--------------------------------------------------------------------------------
+
+the typical `main()` method could be greatly simplified.
+initializing tango, registering tango classes, initializing and running the
+server loop and managing errors could all be done with the single function
+call to :func:`~PyTango.server_run`
+
+Low-level way::
+
+ def main():
+ try:
+ py = PyTango.Util(sys.argv)
+ py.add_class(MotorClass,Motor,'Motor')
+
+ U = PyTango.Util.instance()
+ U.server_init()
+ U.server_run()
+
+ except PyTango.DevFailed,e:
+ print '-------> Received a DevFailed exception:',e
+ except Exception,e:
+ print '-------> An unforeseen exception occured....',e
+
+High-level way::
+
+ def main():
+ classes = Motor,
+ PyTango.server_run(classes)
+
+In practice
+===========
+
Currently, a pogo generated device server code for a Motor having a double
attribute `position` would look like this::
@@ -224,7 +492,8 @@ attribute `position` would look like this::
main()
-To make things clear, let's analyse the stripified version of the code instead::
+To make things more fair, let's analyse the stripified version of the code
+instead::
import PyTango
import sys
@@ -274,7 +543,7 @@ To make things clear, let's analyse the stripified version of the code instead::
'Position':
[[PyTango.DevDouble,
PyTango.SCALAR,
- PyTango.READ_WRITE]],
+ PyTango.READ]],
}
def __init__(self, name):
@@ -300,138 +569,34 @@ To make things clear, let's analyse the stripified version of the code instead::
if __name__ == '__main__':
main()
-After looking at most python device servers one can see some patterns:
-
-At `<Device>` class level:
-
- #. <Device> always inherits from latest available DeviceImpl from pogo version
- #. constructor always does the same:
- #. calls super constructor
- #. debug message
- #. calls init_device
-
- #. all methods have debug_stream as first instruction
- #. init_device does additionaly get_device_properties()
-
-At `<Device>Class` class level:
-
- #. A <Device>Class class exists for every <DeviceName> class
- #. The <Device>Class class only contains attributes, commands and
- properties descriptions (no logic)
- #. The attr_list description always follows the same (non explicit) pattern
- (and so does cmd_list, class_property_list, device_property_list)
- #. the syntax for attr_list, cmd_list, etc is far from understandable
-
-At `main()` level:
-
- #. The main() method always does the same:
- #. create `Util`
- #. register tango class
- #. `server_init()`
- #. `server_run()`
-
-
-
-High level API
-==============
-
-The goals of the high level API are:
-
- #. Automatically inherit from latest DeviceImpl available from current PyTango installation
- #. add default implementation for `init_device`: debug_stream(), get_device_properties()
- #. Hide `<Device>Class` class completely
- #. simplify `main()`
-
-The final code could look something like this::
+And the equivalent API2 version of the code would be::
#!/usr/bin/env python
- from PyTango import server_run
- from PyTango.api2 import Device, DeviceMeta, attribute, command, \
- device_property, class_property
+ from PyTango import DebugIt, server_run
+ from PyTango.api2 import Device, DeviceMeta, attribute
class Motor(Device):
__metaclass__ = DeviceMeta
position = attribute()
-
- def read_position(self, attr):
- attr.set_value(2.3)
-
- @command()
- def move(self, position):
- pass
-
- def main():
- server_run((Motor,))
-
- if __name__ == "__main__":
- main()
-
-Ok, a pogo generated code could look like this::
+ @DebugIt()
+ def read_position(self):
+ return 1.0
- ##############################################################################
- # File : Motor.py
- # Project : motor project
- # Author : tcoutinho
- #============================================================================
- # This file is generated by POGO
- # (Program Obviously used to Generate tango Object)
- #
- # (c) - Software Engineering Group - ESRF
- ##############################################################################
-
- import PyTango
- from PyTango.api2 import Device, DeviceMeta, Attr
-
- # Add additional import
- #----- PROTECTED REGION ID(Motor.additionnal_import) ENABLED START -----#
-
- #----- PROTECTED REGION END -----# // Motor.additionnal_import
-
- class Motor(Device):
- __metaclass__ = DeviceMeta
-
- #--------- Add you global variables here --------------------------
- #----- PROTECTED REGION ID(Motor.global_variables) ENABLED START -----#
-
- #----- PROTECTED REGION END -----# // Motor.global_variables
-
- def init_device(self):
- Device.init_device(self)
- self.attr_Position_read = 0.0
- #----- PROTECTED REGION ID(Motor.init_device) ENABLED START -----#
-
- #----- PROTECTED REGION END -----# // Motor.init_device
-
- # Motor read/write attribute methods
-
- def read_position(self, attr):
- #----- PROTECTED REGION ID(Motor.Position_read) ENABLED START -----#
- self.attr_Position_read = 1.0
- #----- PROTECTED REGION END -----# // Motor.Position_read
- attr.set_value(self.attr_Position_read)
-
-
- position = attribute(name='position',
- dtype=PyTango.DevDouble,
- dformat=PyTango.AttrDataFormat.SCALAR,
- fread=read_position)
-
def main():
- classes = Motor,
- server_run(classes)
+ server_run((Motor,))
if __name__ == "__main__":
main()
-but still is far more inteligable than the original version.
-
References
==========
+:mod:`PyTango.api2`
+
Changes
=======
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/pytango.git
More information about the debian-science-commits
mailing list