Skip to content

Commit

Permalink
Merge pull request #251 from spacetelescope/feature/entry_points_for_…
Browse files Browse the repository at this point in the history
…services_and_proxies

Use entry points for services and service proxies
  • Loading branch information
ehpor authored Nov 12, 2024
2 parents 3e304d6 + 8790269 commit 825ce48
Show file tree
Hide file tree
Showing 18 changed files with 127 additions and 65 deletions.
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/bmc_dm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from astropy.io import fits
import hcipy

@ServiceProxy.register_service_interface('bmc_dm')

class BmcDmProxy(ServiceProxy):
@property
def dm_mask(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import warnings

@ServiceProxy.register_service_interface('camera')

class CameraProxy(ServiceProxy):
def take_raw_exposures(self, num_exposures):
was_acquiring = self.is_acquiring.get()[0]
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/deformable_mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from astropy.io import fits
import hcipy

@ServiceProxy.register_service_interface('deformable_mirror')

class DeformableMirrorProxy(ServiceProxy):
@property
def device_actuator_mask(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/flip_mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

@ServiceProxy.register_service_interface('flip_mount')

class FlipMountProxy(ServiceProxy):
def move_to(self, position, wait=True):
position = self.resolve_position(position)
Expand Down
1 change: 0 additions & 1 deletion catkit2/testbed/proxies/newport_picomotor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
MAX_TIMEOUT_FOR_CHECKING = 1000 # ms


@ServiceProxy.register_service_interface('newport_picomotor')
class NewportPicomotorProxy(ServiceProxy):
log = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/newport_xps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

@ServiceProxy.register_service_interface('newport_xps_q8')

class NewportXpsQ8Proxy(ServiceProxy):
def move_absolute(self, motor_id, position, timeout=None):
command_stream = getattr(self, motor_id.lower() + '_command')
Expand Down
1 change: 0 additions & 1 deletion catkit2/testbed/proxies/ni_daq.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from ..service_proxy import ServiceProxy


@ServiceProxy.register_service_interface('ni_daq')
class NiDaqProxy(ServiceProxy):
def apply_voltage(self, channel, voltage, timeout=None):
getattr(self, channel).submit_data(voltage)
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/nkt_superk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from ..service_proxy import ServiceProxy

@ServiceProxy.register_service_interface('nkt_superk')

class NktSuperkProxy(ServiceProxy):
@property
def center_wavelength(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/oceanoptics_spectrometer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ..service_proxy import ServiceProxy

@ServiceProxy.register_service_interface('oceanoptics_spectrometer')

class OceanopticsSpectroProxy(ServiceProxy):

def take_raw_exposures(self, num_exposures):
Expand Down
1 change: 0 additions & 1 deletion catkit2/testbed/proxies/thorlabs_cube_motor_kinesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import numpy as np


@ServiceProxy.register_service_interface('thorlabs_cube_motor_kinesis')
class ThorlabsCubeMotorKinesisProxy(ServiceProxy):
def move_absolute(self, position):
position = self.resolve_position(position)
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/thorlabs_mcls1.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ..service_proxy import ServiceProxy

@ServiceProxy.register_service_interface('thorlabs_mcls1')

class ThorlabsMcls1(ServiceProxy):
@property
def channel(self):
Expand Down
2 changes: 1 addition & 1 deletion catkit2/testbed/proxies/web_power_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

@ServiceProxy.register_service_interface('web_power_switch')

class WebPowerSwitchProxy(ServiceProxy):
def switch(self, outlet_name, on):
if outlet_name.lower() not in self.outlets:
Expand Down
42 changes: 15 additions & 27 deletions catkit2/testbed/service_proxy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from .. import catkit_bindings

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata


class ServiceProxy(catkit_bindings.ServiceProxy):
'''A proxy for a service connected to a server.
Expand Down Expand Up @@ -90,38 +96,20 @@ def get_service_interface(cls, interface_name):
Parameters
----------
interface_name : string
interface_name : string or None
The name of the interface.
Returns
-------
derived class of ServiceProxy or ServiceProxy
The class belonging to the interface name.
'''
if interface_name in cls._service_interfaces:
return cls._service_interfaces[interface_name]
elif interface_name is None:
return cls
else:
raise AttributeError(f"Service proxy class with interface name '{interface_name}' not found. Did you import it?")

@classmethod
def register_service_interface(cls, interface_name):
'''Register a ServiceProxy derived class.
if interface_name is None:
return ServiceProxy

Parameters
----------
interface_name : string
The name of the interface.
Returns
-------
class decorator
For decorating your ServiceProxy derived class with.
'''
def decorator(interface_class):
cls._service_interfaces[interface_name] = interface_class

return interface_class

return decorator
entry_points = importlib_metadata.entry_points()['catkit2.proxies']
for entry_point in entry_points:
if entry_point.name == interface_name:
return entry_point.load()
else:
raise AttributeError(f"Service proxy class with interface name '{interface_name}' not found. Did you set it as an entry point?")
57 changes: 33 additions & 24 deletions catkit2/testbed/testbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import socket
import threading
import contextlib
import importlib

import psutil
import zmq
Expand All @@ -18,6 +19,11 @@
from ..proto import testbed_pb2 as testbed_proto
from ..proto import service_pb2 as service_proto

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata


SERVICE_LIVELINESS = 5

Expand Down Expand Up @@ -213,10 +219,6 @@ def __init__(self, port, is_simulated, config):

self.log = logging.getLogger(__name__)

self.service_paths = [os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'services'))]
if 'service_paths' in self.config['testbed']:
self.service_paths.extend(self.config['testbed']['service_paths'])

self.startup_services = []
if 'safety' in self.config['testbed']:
self.startup_services.append(self.config['testbed']['safety']['service_id'])
Expand Down Expand Up @@ -274,6 +276,14 @@ def __init__(self, port, is_simulated, config):
for service_id in shut_down_list:
services_to_shut_down.remove(service_id)

# Read in service types.
self.service_type_paths = {}
for entry_point in importlib_metadata.entry_points()["catkit2.services"]:
module = entry_point.module
spec = importlib.util.find_spec(module)
path = os.path.abspath(spec.origin)
self.register_service_type(entry_point.name, path)

# Create server instance and register request handlers.
self.server = Server(port)

Expand Down Expand Up @@ -575,6 +585,18 @@ def on_shut_down(self, data):
reply = testbed_proto.ShutDownReply()
return reply.SerializeToString()

def register_service_type(self, service_type, path):
'''Register a service type.
Parameters
----------
service_type : str
The service type.
path : str
The path to the Python file to run for this service.
'''
self.service_type_paths[service_type] = path

def start_service(self, service_id):
'''Start a service.
Expand Down Expand Up @@ -607,19 +629,11 @@ def start_service(self, service_id):
service_type = self.services[service_id].service_type

# Resolve service type;
dirname = self.resolve_service_type(service_type)

# Find if Python or C++.
if os.path.exists(os.path.join(dirname, service_type + '.py')):
executable = [sys.executable, os.path.join(dirname, service_type + '.py')]
elif os.path.exists(os.path.join(dirname, service_type + '.exe')):
executable = [os.path.join(dirname, service_type + '.exe')]
elif os.path.exists(os.path.join(dirname, service_type)):
executable = [os.path.join(dirname, service_type)]
else:
self.log.warning(f"Could not find the script/executable for service type \"{service_type}\".")
path = self.resolve_service_type(service_type)
dirname = os.path.dirname(path)

raise RuntimeError(f"Service '{service_id}' is not Python or C++.")
# Build Python executable command.
executable = [sys.executable, path]

# Get unused port for this service.
port = get_unused_port()
Expand Down Expand Up @@ -743,17 +757,12 @@ def resolve_service_type(self, service_type):
Returns
-------
string
The path to where the Python script or executable for the
service can be found.
The path to the Python script of the service.
'''
for base_path in self.service_paths:
dirname = os.path.join(base_path, service_type)
if os.path.exists(dirname):
break
else:
if service_type not in self.service_type_paths:
raise ValueError(f"Service type '{service_type}' not recognized.")

return dirname
return self.service_type_paths[service_type]

def shut_down_all_services(self):
'''Shut down all running services.
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies:
- pytest
- flake8
- h5py
- importlib_metadata
- pip:
- dcps
- zwoasi>=0.0.21
63 changes: 63 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,67 @@ def build_extension(self, ext):
cmdclass=dict(build_ext=CMakeBuild),
zip_safe=False,
install_requires=[],
entry_points={
'catkit2.services': [
'aimtti_plp_device = catkit2.services.aimtti_plp_device.aimtti_plp_device',
'aimtti_plp_device_sim = catkit2.services.aimtti_plp_device_sim.aimtti_plp_device_sim',
'allied_vision_camera = catkit2.services.allied_vision_camera.allied_vision_camera',
'bmc_deformable_mirror_hardware = catkit2.services.bmc_deformable_mirror_hardware.bmc_deformable_mirror_hardware',
'bmc_deformable_mirror_sim = catkit2.services.bmc_deformable_mirror_sim.bmc_deformable_mirror_sim',
'bmc_dm = catkit2.services.bmc_dm.bmc_dm',
'bmc_dm_sim = catkit2.services.bmc_dm_sim.bmc_dm_sim',
'camera_sim = catkit2.services.camera_sim.camera_sim',
'dummy_camera = catkit2.services.dummy_camera.dummy_camera',
'empty_service = catkit2.services.empty_service.empty_service',
'flir_camera = catkit2.services.flir_camera.flir_camera',
'hamamatsu_camera = catkit2.services.hamamatsu_camera.hamamatsu_camera',
'newport_picomotor = catkit2.services.newport_picomotor.newport_picomotor',
'newport_picomotor_sim = catkit2.services.newport_picomotor_sim.newport_picomotor_sim',
'newport_xps_q8 = catkit2.services.newport_xps_q8.newport_xps_q8',
'newport_xps_q8_sim = catkit2.services.newport_xps_q8_sim.newport_xps_q8_sim',
'ni_daq = catkit2.services.ni_daq.ni_daq',
'ni_daq_sim = catkit2.services.ni_daq_sim.ni_daq_sim',
'nkt_superk = catkit2.services.nkt_superk.nkt_superk',
'nkt_superk_sim = catkit2.services.nkt_superk_sim.nkt_superk_sim',
'oceanoptics_spectrometer = catkit2.services.oceanoptics_spectrometer.oceanoptics_spectrometer',
'oceanoptics_spectrometer_sim = catkit2.services.oceanoptics_spectrometer_sim.oceanoptics_spectrometer_sim',
'omega_ithx_w3 = catkit2.services.omega_ithx_w3.omega_ithx_w3',
'omega_ithx_w3_sim = catkit2.services.omega_ithx_w3_sim.omega_ithx_w3_sim',
'safety_manual_check = catkit2.services.safety_manual_check.safety_manual_check',
'safety_monitor = catkit2.services.safety_monitor.safety_monitor',
'simple_simulator = catkit2.services.simple_simulator.simple_simulator',
'snmp_ups = catkit2.services.snmp_ups.snmp_ups',
'snmp_ups_sim = catkit2.services.snmp_ups_sim.snmp_ups_sim',
'thorlabs_cld101x = catkit2.services.thorlabs_cld101x.thorlabs_cld101x',
'thorlabs_cld101x_sim = catkit2.services.thorlabs_cld101x_sim.thorlabs_cld101x_sim',
'thorlabs_cube_motor_kinesis = catkit2.services.thorlabs_cube_motor_kinesis.thorlabs_cube_motor_kinesis',
'thorlabs_cube_motor_kinesis_sim = catkit2.services.thorlabs_cube_motor_kinesis_sim.thorlabs_cube_motor_kinesis_sim',
'thorlabs_fw102c = catkit2.services.thorlabs_fw102c.thorlabs_fw102c',
'thorlabs_mcls1 = catkit2.services.thorlabs_mcls1.thorlabs_mcls1',
'thorlabs_mcls1_sim = catkit2.services.thorlabs_mcls1_sim.thorlabs_mcls1_sim',
'thorlabs_mff101 = catkit2.services.thorlabs_mff101.thorlabs_mff101',
'thorlabs_mff101_sim = catkit2.services.thorlabs_mff101_sim.thorlabs_mff101_sim',
'thorlabs_pm = catkit2.services.thorlabs_pm.thorlabs_pm',
'thorlabs_pm_sim = catkit2.services.thorlabs_pm_sim.thorlabs_pm_sim',
'thorlabs_tsp01 = catkit2.services.thorlabs_tsp01.thorlabs_tsp01',
'thorlabs_tsp01_sim = catkit2.services.thorlabs_tsp01_sim.thorlabs_tsp01_sim',
'web_power_switch = catkit2.services.web_power_switch.web_power_switch',
'web_power_switch_sim = catkit2.services.web_power_switch_sim.web_power_switch_sim',
'zwo_camera = catkit2.services.zwo_camera.zwo_camera',
],
'catkit2.proxies': [
'bmc_dm = catkit2.testbed.proxies.bmc_dm:BmcDmProxy',
'camera = catkit2.testbed.proxies.camera:CameraProxy',
'deformable_mirror = catkit2.testbed.proxies.deformable_mirror:DeformableMirrorProxy',
'flip_mount = catkit2.testbed.proxies.flip_mount:FlipMountProxy',
'newport_picomotor = catkit2.testbed.proxies.newport_picomotor:NewportPicomotorProxy',
'newport_xps_q8 = catkit2.testbed.proxies.newport_xps:NewportXpsQ8Proxy',
'ni_daq = catkit2.testbed.proxies.ni_daq:NiDaqProxy',
'nkt_superk = catkit2.testbed.proxies.nkt_superk:NktSuperkProxy',
'oceanoptics_spectrometer = catkit2.testbed.proxies.oceanoptics_spectrometer:OceanopticsSpectroProxy',
'thorlabs_cube_motor_kinesis = catkit2.testbed.proxies.thorlabs_cube_motor_kinesis:ThorlabsCubeMotorKinesisProxy',
'thorlabs_mcls1 = catkit2.testbed.proxies.thorlabs_mcls1:ThorlabsMcls1',
'web_power_switch = catkit2.testbed.proxies.web_power_switch:WebPowerSwitchProxy'
]
}
)
2 changes: 0 additions & 2 deletions tests/config/testbed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ safety:
check_interval: 60
safe_interval: 180

service_paths:
- !path ../services/
base_data_path:
default: !path "~/temp_data"
support_data_path:
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ def run_testbed(port, config):
is_simulated = False

testbed = Testbed(port, is_simulated, config)

# Add test services manually for testing purposes.
base_path = os.path.dirname(__file__)
testbed.register_service_type('dummy_service', os.path.join(base_path, 'services/dummy_service/dummy_service.py'))
testbed.register_service_type('dummy_dm_service', os.path.join(base_path, 'services/dummy_dm_service/dummy_dm_service.py'))

testbed.run()

@pytest.fixture(scope='session')
Expand Down

0 comments on commit 825ce48

Please sign in to comment.