pyacquisition 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. pyacquisition/__init__.py +47 -0
  2. pyacquisition/core/__init__.py +1 -0
  3. pyacquisition/core/adapters/__init__.py +15 -0
  4. pyacquisition/core/adapters/mock.py +120 -0
  5. pyacquisition/core/adapters/prologix.py +0 -0
  6. pyacquisition/core/adapters/pyvisa.py +7 -0
  7. pyacquisition/core/api_server.py +186 -0
  8. pyacquisition/core/broadcaster.py +54 -0
  9. pyacquisition/core/consumer.py +141 -0
  10. pyacquisition/core/experiment.py +366 -0
  11. pyacquisition/core/instrument.py +179 -0
  12. pyacquisition/core/logging.py +147 -0
  13. pyacquisition/core/measurement.py +70 -0
  14. pyacquisition/core/rack.py +229 -0
  15. pyacquisition/core/relay.py +36 -0
  16. pyacquisition/core/response.py +25 -0
  17. pyacquisition/core/scribe.py +231 -0
  18. pyacquisition/core/task.py +181 -0
  19. pyacquisition/core/task_manager.py +206 -0
  20. pyacquisition/gui/__init__.py +303 -0
  21. pyacquisition/gui/api_client.py +254 -0
  22. pyacquisition/gui/components/__init__.py +0 -0
  23. pyacquisition/gui/components/endpoint_popup.py +97 -0
  24. pyacquisition/gui/components/file_window.py +52 -0
  25. pyacquisition/gui/components/input_group.py +43 -0
  26. pyacquisition/gui/components/inputs/__init__.py +0 -0
  27. pyacquisition/gui/components/inputs/base_input.py +37 -0
  28. pyacquisition/gui/components/inputs/boolean_input.py +20 -0
  29. pyacquisition/gui/components/inputs/enum_input.py +32 -0
  30. pyacquisition/gui/components/inputs/float_input.py +18 -0
  31. pyacquisition/gui/components/inputs/integer_input.py +20 -0
  32. pyacquisition/gui/components/inputs/string_input.py +20 -0
  33. pyacquisition/gui/components/live_data_window.py +72 -0
  34. pyacquisition/gui/components/live_log_window.py +72 -0
  35. pyacquisition/gui/components/live_plot.py +110 -0
  36. pyacquisition/gui/components/task_manager_window.py +101 -0
  37. pyacquisition/gui/components/text/__init__.py +57 -0
  38. pyacquisition/gui/constants.py +10 -0
  39. pyacquisition/gui/dataframe.py +58 -0
  40. pyacquisition/gui/openapi.py +370 -0
  41. pyacquisition/instruments/__init__.py +15 -0
  42. pyacquisition/instruments/hardware/__init__.py +0 -0
  43. pyacquisition/instruments/lakeshore/__init__.py +2 -0
  44. pyacquisition/instruments/lakeshore/lakeshore_340.py +417 -0
  45. pyacquisition/instruments/lakeshore/lakeshore_350.py +417 -0
  46. pyacquisition/instruments/oxford_instruments/__init__.py +1 -0
  47. pyacquisition/instruments/oxford_instruments/mercury_ips.py +579 -0
  48. pyacquisition/instruments/software/__init__.py +1 -0
  49. pyacquisition/instruments/software/clock.py +116 -0
  50. pyacquisition/instruments/stanford_research/__init__.py +2 -0
  51. pyacquisition/instruments/stanford_research/sr_830.py +580 -0
  52. pyacquisition/instruments/stanford_research/sr_860.py +674 -0
  53. pyacquisition/py.typed +0 -0
  54. pyacquisition/tasks/__init__.py +11 -0
  55. pyacquisition/tasks/wait.py +74 -0
  56. pyacquisition-0.1.0.dist-info/METADATA +33 -0
  57. pyacquisition-0.1.0.dist-info/RECORD +59 -0
  58. pyacquisition-0.1.0.dist-info/WHEEL +4 -0
  59. pyacquisition-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,47 @@
1
+ from .core import Experiment
2
+ from .core.task import Task
3
+ import sys, asyncio
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class MyTask(Task):
9
+
10
+ name = "MyTask"
11
+ description = "My task description"
12
+ help = "My task help"
13
+
14
+ async def run(self, experiment):
15
+ yield 'STARTING'
16
+ await asyncio.sleep(1)
17
+ yield 'STARTING'
18
+ await asyncio.sleep(1)
19
+ yield 'STARTING'
20
+ await asyncio.sleep(1)
21
+ yield 'STARTING'
22
+ await asyncio.sleep(1)
23
+
24
+
25
+
26
+ class MyExperiment(Experiment):
27
+
28
+
29
+ def setup(self) -> None:
30
+
31
+
32
+ self.register_task(MyTask)
33
+
34
+
35
+
36
+
37
+ def main(*args) -> None:
38
+ """
39
+ Main function to run the experiment.
40
+
41
+ Args:
42
+ toml_file (str): Path to the TOML configuration file.
43
+ """
44
+
45
+ toml_file = " ".join(sys.argv[1:])
46
+ experiment = MyExperiment.from_config(toml_file=toml_file)
47
+ experiment.run()
@@ -0,0 +1 @@
1
+ from .experiment import Experiment
@@ -0,0 +1,15 @@
1
+ from .pyvisa import pyvisa_adapter
2
+ from .mock import mock_adapter
3
+
4
+ _adapters = {
5
+ 'pyvisa': pyvisa_adapter,
6
+ 'mock': mock_adapter,
7
+ }
8
+
9
+ def get_adapter(adapter_name: str):
10
+ """Get the adapter class by name."""
11
+
12
+ if adapter_name in _adapters:
13
+ return _adapters[adapter_name]()
14
+ else:
15
+ raise ValueError(f"Adapter {adapter_name} not found.")
@@ -0,0 +1,120 @@
1
+ def mock_adapter():
2
+ """
3
+ Mock adapter function that returns the input data unchanged.
4
+
5
+ Args:
6
+ data (dict): The input data to be processed.
7
+
8
+ Returns:
9
+ dict: The unchanged input data.
10
+ """
11
+ return MockResourceManager()
12
+
13
+
14
+
15
+
16
+ class MockResource:
17
+ def __init__(
18
+ self,
19
+ resource_name,
20
+ read_termination='',
21
+ write_termination='',
22
+ send_end=True,
23
+ query_delay=0.0,
24
+ ):
25
+ self.resource_name = resource_name
26
+ self.opened = True
27
+ self._timeout = 2000 # Default timeout in ms
28
+ self._read_termination = read_termination
29
+ self._write_termination = write_termination
30
+ self._send_end = send_end
31
+ self._query_delay = query_delay
32
+
33
+ @property
34
+ def timeout(self):
35
+ return self._timeout
36
+
37
+ @timeout.setter
38
+ def timeout(self, value):
39
+ if value is None or value == float('+inf'):
40
+ self._timeout = float('+inf')
41
+ elif value < 1:
42
+ self._timeout = 0 # Immediate
43
+ else:
44
+ self._timeout = value
45
+
46
+ @timeout.deleter
47
+ def timeout(self):
48
+ self._timeout = float('+inf')
49
+
50
+
51
+ @property
52
+ def read_termination(self):
53
+ return self._read_termination
54
+
55
+
56
+ @read_termination.setter
57
+ def read_termination(self, value):
58
+ self._read_termination = value
59
+
60
+
61
+ @property
62
+ def write_termination(self):
63
+ return self._write_termination
64
+
65
+
66
+ @write_termination.setter
67
+ def write_termination(self, value):
68
+ self._write_termination = value
69
+
70
+
71
+ @property
72
+ def send_end(self):
73
+ return self._send_end
74
+
75
+
76
+ @send_end.setter
77
+ def send_end(self, value):
78
+ self._send_end = bool(value)
79
+
80
+
81
+ @property
82
+ def query_delay(self):
83
+ return self._query_delay
84
+
85
+
86
+ @query_delay.setter
87
+ def query_delay(self, value):
88
+ self._query_delay = float(value)
89
+
90
+
91
+ def write(self, command):
92
+ return f"Mock write to {self.resource_name}: {command}"
93
+
94
+
95
+ def read(self):
96
+ return f"Mock read from {self.resource_name}"
97
+
98
+
99
+ def close(self):
100
+ self.opened = False
101
+ return f"Mock resource {self.resource_name} closed"
102
+
103
+
104
+ class MockResourceManager:
105
+ def __init__(self):
106
+ self.resources = {}
107
+
108
+ def open_resource(self, resource_name):
109
+ resource = MockResource(resource_name)
110
+ self.resources[resource_name] = resource
111
+ return resource
112
+
113
+ def list_resources(self):
114
+ # Return a list of mock resource names
115
+ return tuple(self.resources.keys())
116
+
117
+ def close(self):
118
+ for resource in self.resources.values():
119
+ resource.close()
120
+ self.resources.clear()
File without changes
@@ -0,0 +1,7 @@
1
+ from pyvisa import ResourceManager
2
+
3
+
4
+ def pyvisa_adapter():
5
+ rm = ResourceManager()
6
+ print(rm.list_resources())
7
+ return rm
@@ -0,0 +1,186 @@
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
2
+ from websockets.exceptions import ConnectionClosedOK
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import uvicorn
5
+ import asyncio
6
+ from .logging import logger
7
+ from .consumer import Consumer
8
+ from .response import StringResponse, DictResponse
9
+ from enum import Enum
10
+
11
+
12
+ class WebsocketEndpoint(Consumer):
13
+ """
14
+ A class that handles WebSocket connections and data streaming.
15
+ """
16
+
17
+
18
+ @staticmethod
19
+ def _enum_to_selected_dict(enum_instance):
20
+ """
21
+ Converts an enum instance to a dictionary with enum names as keys and their values as values.
22
+ """
23
+ return {
24
+ item.name: {
25
+ "value": item.value,
26
+ "selected": item == enum_instance,
27
+ } for item in enum_instance.__class__
28
+ }
29
+
30
+
31
+ async def run(self, websocket: WebSocket):
32
+ """
33
+ Start the WebSocket server and listen for incoming connections.
34
+ """
35
+
36
+ await websocket.accept()
37
+ logger.debug("[FastApi] Client connected")
38
+ try:
39
+ while True:
40
+ data = await self.consume()
41
+ for key, value in data.items():
42
+ if isinstance(value, Enum):
43
+ data[key] = APIServer._enum_to_selected_dict(value)
44
+ await websocket.send_json(data)
45
+ except WebSocketDisconnect:
46
+ logger.debug("[FastApi] Client disconnected")
47
+ except ConnectionClosedOK:
48
+ logger.debug("[FastApi] Connection closed normally")
49
+ except Exception as e:
50
+ logger.error(f"[FastApi] An error occurred: {e}")
51
+ await websocket.close()
52
+
53
+
54
+
55
+ class APIServer:
56
+
57
+
58
+ def __init__(
59
+ self,
60
+ host: str = "localhost",
61
+ port: int = 8000,
62
+ #allowed_cors_origins: list = ["http://localhost:3000"],
63
+ ):
64
+
65
+ self.host = host
66
+ self.port = port
67
+
68
+ self.app = FastAPI(
69
+ title="PyAcquisition API",
70
+ description="API for PyAcquisition",
71
+ )
72
+
73
+ self.websocket_endpoints = {}
74
+
75
+ # self.app.add_middleware(
76
+ # CORSMiddleware,
77
+ # allow_origins=allowed_cors_origins,
78
+ # allow_credentials=True,
79
+ # allow_methods=["*"],
80
+ # allow_headers=["*"],
81
+ # )
82
+
83
+ logger.debug("[FastApi] APIServer initialized")
84
+
85
+
86
+
87
+
88
+ @staticmethod
89
+ def _enum_to_selected_dict(enum_instance):
90
+ """
91
+ Converts an enum instance to a dictionary with enum names as keys and their values as values.
92
+ """
93
+ return {
94
+ item.name: {
95
+ "value": item.value,
96
+ "selected": item == enum_instance,
97
+ } for item in enum_instance.__class__
98
+ }
99
+
100
+
101
+ def add_websocket_endpoint(self, url: str):
102
+ """
103
+ Adds a WebSocket endpoint to the FastAPI app.
104
+
105
+ Args:
106
+ url (str): The URL path for the WebSocket endpoint.
107
+ """
108
+
109
+ self.websocket_endpoints[url] = WebsocketEndpoint()
110
+
111
+ @self.app.websocket(url)
112
+ async def websocket_endpoint(websocket: WebSocket):
113
+ """
114
+ WebSocket endpoint that polls the provided async function and sends data to connected clients.
115
+
116
+ Args:
117
+ websocket (WebSocket): WebSocket connection object.
118
+ """
119
+ await self.websocket_endpoints[url].run(websocket)
120
+
121
+ logger.debug(f"[FastApi] WebSocket endpoint added at '{url}'")
122
+
123
+
124
+ def setup(self):
125
+ """
126
+ Sets up the API server. This method is called before running the server.
127
+ """
128
+ logger.debug(f"[FastApi] Server setup started at {self.host}:{self.port}")
129
+ logger.debug("[FastApi] Server setup completed")
130
+
131
+
132
+ def run(self):
133
+ """
134
+ A coroutine that runs the FastAPI server.
135
+ """
136
+ try:
137
+ config = uvicorn.Config(
138
+ self.app,
139
+ host=self.host,
140
+ port=self.port,
141
+ log_level="warning",
142
+ )
143
+ server = uvicorn.Server(config)
144
+ return server.serve()
145
+ except Exception as e:
146
+ # Log the exception or handle it as needed
147
+ logger.error(f"[FastApi] An error occurred while running the server: {e}")
148
+ return None
149
+
150
+
151
+ def teardown(self):
152
+ """
153
+ Cleans up the API server. This method is called after the server has stopped.
154
+ """
155
+ logger.debug("[FastApi] Server teardown started")
156
+ logger.debug("[FastApi] Server teardown completed")
157
+
158
+
159
+ def register_endpoints(self, api_server):
160
+ """
161
+ Registers endpoints to the FastAPI app.
162
+ """
163
+
164
+
165
+ @api_server.app.get("/ping")
166
+ async def ping() -> DictResponse:
167
+ """
168
+ Endpoint to check if the API server is running.
169
+ """
170
+ return DictResponse(
171
+ status=200,
172
+ data={"message": "Pong!"},
173
+ )
174
+
175
+
176
+ @api_server.app.get("/list_websockets")
177
+ async def list_websockets() -> DictResponse:
178
+ """
179
+ Endpoint to list all available WebSocket endpoints.
180
+ """
181
+ return DictResponse(
182
+ status=200,
183
+ data={"websockets": list(api_server.websocket_endpoints.keys())},
184
+ )
185
+
186
+
@@ -0,0 +1,54 @@
1
+ import asyncio
2
+
3
+ class Broadcaster:
4
+ """
5
+ A class responsible for broadcasting messages to subscribed consumers.
6
+ """
7
+
8
+ def __init__(self):
9
+ """
10
+ Initialize the Broadcaster.
11
+ """
12
+ self._subscribers = []
13
+
14
+
15
+ def subscribe(self, consumer):
16
+ """
17
+ Subscribe a consumer to this broadcaster.
18
+
19
+ Args:
20
+ consumer (Consumer): The consumer to subscribe.
21
+ """
22
+ self._subscribers.append(consumer)
23
+
24
+
25
+ def unsubscribe(self, consumer):
26
+ """
27
+ Unsubscribe a consumer from this broadcaster.
28
+
29
+ Args:
30
+ consumer (Consumer): The consumer to unsubscribe.
31
+ """
32
+ self._subscribers.remove(consumer)
33
+
34
+
35
+ async def broadcast(self, message):
36
+ """
37
+ Broadcast a message to all subscribed consumers.
38
+
39
+ Args:
40
+ message (Any): The message to broadcast.
41
+ """
42
+ for subscriber in self._subscribers:
43
+ await subscriber.queue.put(message)
44
+
45
+
46
+ def broadcast_sync(self, message):
47
+ """
48
+ Broadcast a message to all subscribed consumers synchronously.
49
+
50
+ Args:
51
+ message (Any): The message to broadcast.
52
+ """
53
+ for subscriber in self._subscribers:
54
+ subscriber.queue.put_nowait(message)
@@ -0,0 +1,141 @@
1
+ import asyncio
2
+ from .logging import logger
3
+
4
+
5
+ class Consumer:
6
+ """
7
+ A consumer class that can subscribe and unsubscribe to a Broadcaster.
8
+ """
9
+
10
+ def __init__(self, callbacks=[], async_callbacks=[]):
11
+ """
12
+ Initialize the Consumer.
13
+ """
14
+ self.queue = asyncio.Queue()
15
+ self._callbacks = callbacks
16
+ self._async_callbacks = async_callbacks
17
+
18
+
19
+ def _execute_callbacks(self, message):
20
+ """
21
+ Execute all registered callbacks with the given message.
22
+
23
+ Args:
24
+ message (Any): The message to pass to the callbacks.
25
+ """
26
+ for callback in self._callbacks:
27
+ callback(message)
28
+
29
+
30
+ async def _execute_async_callbacks(self, message):
31
+ """
32
+ Execute all registered async callbacks with the given message.
33
+
34
+ Args:
35
+ message (Any): The message to pass to the async callbacks.
36
+ """
37
+ for async_callback in self._async_callbacks:
38
+ await async_callback(message)
39
+
40
+
41
+ def add_callback(self, callback):
42
+ """
43
+ Add a callback to be executed when a message is consumed.
44
+
45
+ Args:
46
+ callback (callable): The callback function to add.
47
+ """
48
+ self._callbacks.append(callback)
49
+
50
+
51
+ def remove_callback(self, callback):
52
+ """
53
+ Remove a callback from the list of callbacks.
54
+
55
+ Args:
56
+ callback (callable): The callback function to remove.
57
+ """
58
+ if callback in self._callbacks:
59
+ self._callbacks.remove(callback)
60
+
61
+
62
+ def add_async_callback(self, async_callback):
63
+ """
64
+ Add an async callback to be executed when a message is consumed.
65
+
66
+ Args:
67
+ async_callback (callable): The async callback function to add.
68
+ """
69
+ self._async_callbacks.append(async_callback)
70
+
71
+
72
+ def remove_async_callback(self, async_callback):
73
+ """
74
+ Remove an async callback from the list of async callbacks.
75
+
76
+ Args:
77
+ async_callback (callable): The async callback function to remove.
78
+ """
79
+ if async_callback in self._async_callbacks:
80
+ self._async_callbacks.remove(async_callback)
81
+
82
+
83
+ def subscribe_to(self, broadcaster):
84
+ """
85
+ Subscribe to a Broadcaster.
86
+
87
+ Args:
88
+ broadcaster (Broadcaster): The broadcaster to subscribe to.
89
+ """
90
+ broadcaster.subscribe(self)
91
+
92
+
93
+ def unsubscribe(self, broadcaster):
94
+ """
95
+ Unsubscribe from a Broadcaster.
96
+
97
+ Args:
98
+ broadcaster (Broadcaster): The broadcaster to unsubscribe from.
99
+ """
100
+ broadcaster.unsubscribe(self)
101
+
102
+
103
+ async def consume(self, timeout=None):
104
+ """
105
+ Consume a single message from the queue with a timeout.
106
+
107
+ Args:
108
+ timeout (float or None): The maximum time (in seconds) to wait for a message. Defaults to None (no timeout).
109
+
110
+ Returns:
111
+ Any: The message from the queue, or None if the timeout is reached.
112
+ """
113
+ try:
114
+ message = await asyncio.wait_for(self.queue.get(), timeout=timeout)
115
+ self._execute_callbacks(message)
116
+ await self._execute_async_callbacks(message)
117
+ return message
118
+ except asyncio.TimeoutError:
119
+ return None
120
+ except Exception as e:
121
+ logger.error(f"Error consuming message: {e}")
122
+ return None
123
+
124
+
125
+ async def consume_all(self, timeout=None):
126
+ """
127
+ Consume all messages from the queue with a timeout.
128
+
129
+ Args:
130
+ timeout (float or None): The maximum time (in seconds) to wait for messages. Defaults to None (no timeout).
131
+
132
+ Returns:
133
+ list: A list of messages from the queue.
134
+ """
135
+ messages = []
136
+ while True:
137
+ message = await self.consume(timeout=timeout)
138
+ if message is None:
139
+ break
140
+ messages.append(message)
141
+ return messages