Skip to content

Commit 6f1beac

Browse files
committed
Add proposal API, launcher
1 parent cec9fda commit 6f1beac

39 files changed

+630
-169
lines changed

src/fastcs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
"""
88

99
from ._version import __version__
10+
from .main import FastCS as FastCS
11+
from .main import launch as launch
1012

1113
__all__ = ["__version__"]

src/fastcs/backend.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from concurrent.futures import Future
55
from types import MethodType
66

7-
from softioc.asyncio_dispatcher import AsyncioDispatcher
7+
from fastcs.util import AsyncioDispatcher
88

99
from .attributes import AttrR, AttrW, Sender, Updater
1010
from .controller import Controller
@@ -14,10 +14,12 @@
1414

1515
class Backend:
1616
def __init__(
17-
self, controller: Controller, loop: asyncio.AbstractEventLoop | None = None
17+
self,
18+
controller: Controller,
19+
loop: asyncio.AbstractEventLoop | None = None,
1820
):
19-
self._dispatcher = AsyncioDispatcher(loop)
20-
self._loop = self._dispatcher.loop
21+
self.dispatcher = AsyncioDispatcher(loop)
22+
self._loop = self.dispatcher.loop
2123
self._controller = controller
2224

2325
self._initial_coros = [controller.connect]
@@ -27,17 +29,17 @@ def __init__(
2729
self._controller.initialise(), self._loop
2830
).result()
2931

30-
self._mapping = Mapping(self._controller)
32+
self.mapping = Mapping(self._controller)
3133
self._link_process_tasks()
3234

33-
self._context = {
34-
"dispatcher": self._dispatcher,
35+
self.context = {
36+
"dispatcher": self.dispatcher,
3537
"controller": self._controller,
36-
"mapping": self._mapping,
38+
"mapping": self.mapping,
3739
}
3840

3941
def _link_process_tasks(self):
40-
for single_mapping in self._mapping.get_controller_mappings():
42+
for single_mapping in self.mapping.get_controller_mappings():
4143
_link_single_controller_put_tasks(single_mapping)
4244
_link_attribute_sender_class(single_mapping)
4345

@@ -47,7 +49,6 @@ def __del__(self):
4749
def run(self):
4850
self._run_initial_futures()
4951
self.start_scan_futures()
50-
self._run()
5152

5253
def _run_initial_futures(self):
5354
for coro in self._initial_coros:
@@ -57,7 +58,7 @@ def _run_initial_futures(self):
5758
def start_scan_futures(self):
5859
self._scan_futures = {
5960
asyncio.run_coroutine_threadsafe(coro(), self._loop)
60-
for coro in _get_scan_coros(self._mapping)
61+
for coro in _get_scan_coros(self.mapping)
6162
}
6263

6364
def stop_scan_futures(self):
@@ -68,9 +69,6 @@ def stop_scan_futures(self):
6869
except asyncio.CancelledError:
6970
pass
7071

71-
def _run(self):
72-
raise NotImplementedError("Specific Backend must implement _run")
73-
7472

7573
def _link_single_controller_put_tasks(single_mapping: SingleMapping) -> None:
7674
for name, method in single_mapping.put_methods.items():

src/fastcs/backends/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/fastcs/backends/asyncio_backend.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/fastcs/backends/epics/backend.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/fastcs/backends/tango/backend.py

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/fastcs/connections/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
from .ip_connection import IPConnection
2+
from .ip_connection import IPConnectionSettings as IPConnectionSettings
3+
from .ip_connection import StreamConnection as StreamConnection
4+
from .serial_connection import SerialConnection as SerialConnection
5+
from .serial_connection import SerialConnectionSettings as SerialConnectionSettings
26

37
__all__ = ["IPConnection"]

src/fastcs/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
class FastCSException(Exception):
22
pass
3+
4+
5+
class LaunchError(FastCSException):
6+
pass

src/fastcs/main.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import inspect
2+
import json
3+
from pathlib import Path
4+
from typing import Annotated, TypeAlias, get_type_hints
5+
6+
import typer
7+
from pydantic import create_model
8+
from ruamel.yaml import YAML
9+
10+
from .backend import Backend
11+
from .controller import Controller
12+
from .exceptions import LaunchError
13+
from .transport.adapter import TransportAdapter
14+
from .transport.epics.options import EpicsOptions
15+
from .transport.rest.options import RestOptions
16+
from .transport.tango.options import TangoOptions
17+
18+
# Define a type alias for transport options
19+
TransportOptions: TypeAlias = EpicsOptions | TangoOptions | RestOptions
20+
21+
22+
class FastCS:
23+
def __init__(
24+
self,
25+
controller: Controller,
26+
transport_options: TransportOptions,
27+
):
28+
self._backend = Backend(controller)
29+
self._transport: TransportAdapter
30+
match transport_options:
31+
case EpicsOptions():
32+
from .transport.epics.adapter import EpicsTransport
33+
34+
self._transport = EpicsTransport(
35+
self._backend.mapping,
36+
self._backend.context,
37+
self._backend.dispatcher,
38+
transport_options,
39+
)
40+
case TangoOptions():
41+
from .transport.tango.adapter import TangoTransport
42+
43+
self._transport = TangoTransport(
44+
self._backend.mapping,
45+
transport_options,
46+
)
47+
case RestOptions():
48+
from .transport.rest.adapter import RestTransport
49+
50+
self._transport = RestTransport(
51+
self._backend.mapping,
52+
transport_options,
53+
)
54+
55+
def create_docs(self) -> None:
56+
self._transport.create_docs()
57+
58+
def create_gui(self) -> None:
59+
self._transport.create_gui()
60+
61+
def run(self) -> None:
62+
self._backend.run()
63+
self._transport.run()
64+
65+
66+
def launch(controller_class: type[Controller]) -> None:
67+
"""
68+
Serves as an entry point for starting FastCS applications.
69+
70+
By utilizing type hints in a Controller's __init__ method, this
71+
function provides a command-line interface to describe and gather the
72+
required configuration before instantiating the application.
73+
74+
Args:
75+
controller_class (type[Controller]): The FastCS Controller to instantiate.
76+
It must have a type-hinted __init__ method and no more than 2 arguments.
77+
78+
Raises:
79+
LaunchError: If the class's __init__ is not as expected
80+
81+
Example of the expected Controller implementation:
82+
class MyController(Controller):
83+
def __init__(self, my_arg: MyControllerOptions) -> None:
84+
...
85+
86+
Typical usage:
87+
if __name__ == "__main__":
88+
launch(MyController)
89+
"""
90+
_launch(controller_class)()
91+
92+
93+
def _launch(controller_class: type[Controller]) -> typer.Typer:
94+
sig = inspect.signature(controller_class.__init__)
95+
args = inspect.getfullargspec(controller_class.__init__)[0]
96+
if len(args) == 1:
97+
fastcs_options = create_model(
98+
f"{controller_class.__name__}",
99+
transport=(TransportOptions, ...),
100+
__config__={"extra": "forbid"},
101+
)
102+
elif len(args) == 2:
103+
hints = get_type_hints(controller_class.__init__)
104+
if hints:
105+
options_type = list(hints.values())[-1]
106+
else:
107+
raise LaunchError(
108+
f"Expected typehinting in '{controller_class.__name__}"
109+
f".__init__' but received {sig}. Add a typehint for `{args[-1]}`."
110+
)
111+
fastcs_options = create_model(
112+
f"{controller_class.__name__}",
113+
controller=(options_type, ...),
114+
transport=(TransportOptions, ...),
115+
__config__={"extra": "forbid"},
116+
)
117+
else:
118+
raise LaunchError(
119+
f"Expected no more than 2 arguments for '{controller_class.__name__}"
120+
f".__init__' but received {len(args)} as `{sig}`"
121+
)
122+
123+
launch_typer = typer.Typer()
124+
125+
class LaunchContext:
126+
def __init__(self, controller_class, fastcs_options):
127+
self.controller_class = controller_class
128+
self.fastcs_options = fastcs_options
129+
130+
@launch_typer.callback()
131+
def create_context(ctx: typer.Context):
132+
ctx.obj = LaunchContext(
133+
controller_class,
134+
fastcs_options,
135+
)
136+
137+
@launch_typer.command(help=f"Produce json schema for a {controller_class.__name__}")
138+
def schema(ctx: typer.Context):
139+
system_schema = ctx.obj.fastcs_options.model_json_schema()
140+
print(json.dumps(system_schema, indent=2))
141+
142+
@launch_typer.command(help=f"Start up a {controller_class.__name__}")
143+
def run(
144+
ctx: typer.Context,
145+
config: Annotated[
146+
Path,
147+
typer.Argument(
148+
help=f"A yaml file matching the {controller_class.__name__} schema"
149+
),
150+
],
151+
):
152+
"""
153+
Start the controller
154+
"""
155+
controller_class = ctx.obj.controller_class
156+
fastcs_options = ctx.obj.fastcs_options
157+
158+
yaml = YAML(typ="safe")
159+
options_yaml = yaml.load(config)
160+
# To do: Handle a k8s "values.yaml" file
161+
instance_options = fastcs_options.model_validate(options_yaml)
162+
if hasattr(instance_options, "controller"):
163+
controller = controller_class(instance_options.controller)
164+
else:
165+
controller = controller_class()
166+
167+
instance = FastCS(
168+
controller,
169+
instance_options.transport,
170+
)
171+
172+
if "gui" in options_yaml["transport"]:
173+
instance.create_gui()
174+
if "docs" in options_yaml["transport"]:
175+
instance.create_docs()
176+
instance.run()
177+
178+
return launch_typer

src/fastcs/transport/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .epics.options import EpicsDocsOptions as EpicsDocsOptions
2+
from .epics.options import EpicsGUIOptions as EpicsGUIOptions
3+
from .epics.options import EpicsIOCOptions as EpicsIOCOptions
4+
from .epics.options import EpicsOptions as EpicsOptions
5+
from .tango.options import TangoDSROptions as TangoDSROptions
6+
from .tango.options import TangoOptions as TangoOptions
7+
8+
__all__ = ["EpicsOptions", "TangoOptions"]

0 commit comments

Comments
 (0)