pyobs-fli 2.0.0.dev11__tar.gz → 2.0.0.dev12__tar.gz

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 (82) hide show
  1. pyobs_fli-2.0.0.dev12/.github/workflows/dependabot-automerge.yml +24 -0
  2. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/PKG-INFO +1 -1
  3. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/flibase.py +24 -5
  4. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/flifilterwheel.py +30 -2
  5. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyproject.toml +1 -1
  6. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/tests/test_flicamera.py +2 -2
  7. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/tests/test_flifilterwheel.py +8 -4
  8. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/uv.lock +1 -1
  9. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.github/dependabot.yml +0 -0
  10. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.github/workflows/pypi.yml +0 -0
  11. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.github/workflows/pyrefly.yml +0 -0
  12. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.github/workflows/pytest.yml +0 -0
  13. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.github/workflows/ruff.yml +0 -0
  14. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.pre-commit-config.yaml +0 -0
  15. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/.readthedocs.yml +0 -0
  16. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/CHANGELOG.rst +0 -0
  17. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/CMakeLists.txt +0 -0
  18. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/LICENSE +0 -0
  19. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/README.md +0 -0
  20. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/docs/Makefile +0 -0
  21. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/docs/make.bat +0 -0
  22. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/docs/requirements.txt +0 -0
  23. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/docs/source/_static/pyobs.gif +0 -0
  24. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/docs/source/conf.py +0 -0
  25. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/docs/source/index.rst +0 -0
  26. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/Makefile +0 -0
  27. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/docxx.sty +0 -0
  28. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-camera-parport.c +0 -0
  29. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-camera-parport.h +0 -0
  30. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-camera-usb.c +0 -0
  31. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-camera-usb.h +0 -0
  32. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-camera.c +0 -0
  33. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-camera.h +0 -0
  34. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-debug.h +0 -0
  35. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-filter-focuser.c +0 -0
  36. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-filter-focuser.h +0 -0
  37. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-libfli.h +0 -0
  38. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-mem.c +0 -0
  39. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-mem.h +0 -0
  40. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-raw.c +0 -0
  41. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli-raw.h +0 -0
  42. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli.c +0 -0
  43. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli.dxx +0 -0
  44. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/libfli.h +0 -0
  45. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/bsd/libfli-usb-sys.c +0 -0
  46. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-debug.c +0 -0
  47. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-parport.h +0 -0
  48. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-serial.c +0 -0
  49. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-serial.h +0 -0
  50. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-sys.c +0 -0
  51. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-sys.h +0 -0
  52. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-usb.c +0 -0
  53. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/libfli-usb.h +0 -0
  54. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/linux/fli_ioctl.h +0 -0
  55. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/linux/fliusb_ioctl.h +0 -0
  56. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/linux/libfli-parport.c +0 -0
  57. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/linux/libfli-usb-sys.c +0 -0
  58. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/osx/libfli-usb-sys.c +0 -0
  59. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/unix/osx/libfli-usb-sys.h +0 -0
  60. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-debug.c +0 -0
  61. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-parport.h +0 -0
  62. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-serial.c +0 -0
  63. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-serial.h +0 -0
  64. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-sys.h +0 -0
  65. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-usb.c +0 -0
  66. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-usb.h +0 -0
  67. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-windows-parport.c +0 -0
  68. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli-windows.c +0 -0
  69. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli.def +0 -0
  70. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli.rc +0 -0
  71. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli.sln +0 -0
  72. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli.vcproj +0 -0
  73. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/libfli64.def +0 -0
  74. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/lib/windows/resource.h +0 -0
  75. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/poetry.lock +0 -0
  76. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/__init__.py +0 -0
  77. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/flicamera.py +0 -0
  78. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/flidriver.pyx +0 -0
  79. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/gui.py +0 -0
  80. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/pyobs_fli/libfli.pxd +0 -0
  81. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/specs/plans/2026-08-16-nogil-fli-driver-calls.md +0 -0
  82. {pyobs_fli-2.0.0.dev11 → pyobs_fli-2.0.0.dev12}/tests/test_import.py +0 -0
@@ -0,0 +1,24 @@
1
+ name: Dependabot auto-merge
2
+
3
+ on: pull_request
4
+
5
+ permissions:
6
+ contents: write
7
+ pull-requests: write
8
+
9
+ jobs:
10
+ dependabot:
11
+ runs-on: ubuntu-latest
12
+ if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
13
+ steps:
14
+ - name: Fetch Dependabot metadata
15
+ id: metadata
16
+ uses: dependabot/fetch-metadata@v2
17
+ with:
18
+ compat-lookup: true
19
+ - name: Enable auto-merge for patch and minor updates
20
+ if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' }}
21
+ run: gh pr merge --auto --merge "$PR_URL"
22
+ env:
23
+ PR_URL: ${{ github.event.pull_request.html_url }}
24
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-fli
3
- Version: 2.0.0.dev11
3
+ Version: 2.0.0.dev12
4
4
  Summary: pyobs module for FLI cameras
5
5
  Author-Email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -27,9 +27,10 @@ class FliBaseMixin:
27
27
  dev_name: str | None = None,
28
28
  dev_path: str | None = None,
29
29
  keep_alive_ping: int = 10,
30
+ sdk_call_timeout: float = _SDK_CALL_TIMEOUT,
30
31
  **kwargs: Any,
31
32
  ):
32
- """Initializes a new FliCamera.
33
+ """Initializes a new FLI device mixin.
33
34
 
34
35
  If neither dev_name nor dev_path are given, the first found device is used.
35
36
 
@@ -38,6 +39,7 @@ class FliBaseMixin:
38
39
  dev_name: Optional name for device.
39
40
  dev_path: Optional path to device.
40
41
  keep_alive_ping: Interval for keep alive ping.
42
+ sdk_call_timeout: Default timeout for blocking FLI SDK calls.
41
43
  """
42
44
  from .flidriver import FliDriver # type: ignore
43
45
 
@@ -46,6 +48,7 @@ class FliBaseMixin:
46
48
  self._dev_name = dev_name
47
49
  self._dev_path = dev_path
48
50
  self._keep_alive_ping = keep_alive_ping
51
+ self._sdk_call_timeout = sdk_call_timeout
49
52
  self._driver: FliDriver | None = None
50
53
  self._device: Any | None = None
51
54
 
@@ -57,24 +60,35 @@ class FliBaseMixin:
57
60
  # claim its own kwargs cooperatively instead of this mixin silently absorbing them
58
61
  super().__init__(**kwargs) # type: ignore[call-arg]
59
62
 
60
- @staticmethod
61
- async def _run_blocking(func: Callable[[], None], timeout: float = _SDK_CALL_TIMEOUT) -> bool:
63
+ async def _run_blocking(self, func: Callable[[], None], timeout: float | None = None) -> bool:
62
64
  """Run a blocking FLI SDK call in a daemon thread, so a hung call can't freeze the module.
63
65
 
64
66
  A plain executor isn't used here, since its worker threads are non-daemon and Python joins
65
67
  them on interpreter shutdown -- a hung call would then just move the freeze to process exit.
66
68
 
69
+ Args:
70
+ func: Blocking callable to run off the event loop.
71
+ timeout: Seconds to wait for completion. Defaults to sdk_call_timeout.
72
+
67
73
  Returns:
68
74
  True if func completed within timeout, False if it's still running in the background.
69
75
  """
76
+ timeout = self._sdk_call_timeout if timeout is None else timeout
70
77
  loop = asyncio.get_running_loop()
71
78
  future: asyncio.Future[None] = loop.create_future()
72
79
 
80
+ def _complete() -> None:
81
+ # The worker thread may outlive the timeout (wait_for cancels the future on
82
+ # timeout), so guard against a late set_result on an already-completed future --
83
+ # otherwise the loop callback raises InvalidStateError.
84
+ if not future.done():
85
+ future.set_result(None)
86
+
73
87
  def _wrapper() -> None:
74
88
  try:
75
89
  func()
76
90
  finally:
77
- loop.call_soon_threadsafe(future.set_result, None)
91
+ loop.call_soon_threadsafe(_complete)
78
92
 
79
93
  threading.Thread(target=_wrapper, daemon=True).start()
80
94
  try:
@@ -83,14 +97,19 @@ class FliBaseMixin:
83
97
  except TimeoutError:
84
98
  return False
85
99
 
86
- async def _run_blocking_or_raise(self, func: Callable[[], _T], timeout: float = _SDK_CALL_TIMEOUT) -> _T:
100
+ async def _run_blocking_or_raise(self, func: Callable[[], _T], timeout: float | None = None) -> _T:
87
101
  """Run a blocking FLI SDK call in a thread, returning its result or re-raising what it raised.
88
102
 
89
103
  Unlike _run_blocking(), this also carries the callable's return value/exception back to the
90
104
  caller -- several FLI calls drive control flow via their return value or a raised ValueError
91
105
  (e.g. device enumeration, readout), which a bare fire-and-forget thread call would otherwise
92
106
  silently lose.
107
+
108
+ Args:
109
+ func: Blocking callable to run off the event loop.
110
+ timeout: Seconds to wait for completion. Defaults to sdk_call_timeout.
93
111
  """
112
+ timeout = self._sdk_call_timeout if timeout is None else timeout
94
113
  outcome: list[Any] = []
95
114
 
96
115
  def _wrapper() -> None:
@@ -16,17 +16,28 @@ from pyobs_fli.flidriver import DeviceType
16
16
 
17
17
  log = logging.getLogger(__name__)
18
18
 
19
+ # A physical filter move can take far longer than a typical SDK call (a long rotation
20
+ # can be several seconds), so give it its own generous, configurable deadline instead of
21
+ # the short SDK-call default.
22
+ _FILTER_MOVE_TIMEOUT = 120.0
23
+
19
24
 
20
25
  class FliFilterWheel(Module, FliBaseMixin, MotionStatusMixin, IFilters, IFitsHeaderBefore):
21
26
  """A pyobs module for FLI filter wheels."""
22
27
 
23
28
  __module__ = "pyobs_fli"
24
29
 
25
- def __init__(self, filter_names: list[str] | list[list[str]], **kwargs: Any):
30
+ def __init__(
31
+ self,
32
+ filter_names: list[str] | list[list[str]],
33
+ filter_move_timeout: float = _FILTER_MOVE_TIMEOUT,
34
+ **kwargs: Any,
35
+ ):
26
36
  """Initializes a new FliFilterWheel.
27
37
 
28
38
  Args:
29
39
  filter_names: Names of filters.
40
+ filter_move_timeout: Seconds to wait for a filter move to complete.
30
41
  """
31
42
  super().__init__(dev_type=DeviceType.FILTERWHEEL, motion_status_interfaces=["IFilters"], **kwargs)
32
43
 
@@ -34,6 +45,7 @@ class FliFilterWheel(Module, FliBaseMixin, MotionStatusMixin, IFilters, IFitsHea
34
45
  [filter_names] if isinstance(filter_names[0], str) else filter_names # type: ignore[arg-type,list-item]
35
46
  )
36
47
 
48
+ self._filter_move_timeout = filter_move_timeout
37
49
  self._current_filter = ""
38
50
 
39
51
  async def open(self) -> None:
@@ -95,7 +107,23 @@ class FliFilterWheel(Module, FliBaseMixin, MotionStatusMixin, IFilters, IFitsHea
95
107
  def _set() -> None:
96
108
  driver.set_filter_pos(pos)
97
109
 
98
- await self._run_blocking_or_raise(_set)
110
+ try:
111
+ # A physical filter move can legitimately take far longer than a typical SDK
112
+ # call, so bound it with a dedicated, configurable move timeout rather than the
113
+ # short SDK-call default -- which cut off long rotations mid-move.
114
+ await self._run_blocking_or_raise(_set, timeout=self._filter_move_timeout)
115
+
116
+ # Confirm the wheel actually reports the requested position before declaring
117
+ # success, so an SDK call that returned without the wheel having arrived can't
118
+ # leave the published filter state silently wrong.
119
+ actual = await self._run_blocking_or_raise(driver.get_filter_pos)
120
+ if actual != pos:
121
+ raise exc.MoveError(f"Filter wheel reported position {actual} after moving to {pos}.")
122
+ except Exception:
123
+ # Don't leave the wheel stuck reporting "slewing" after a failed move.
124
+ await self._change_motion_status(MotionStatus.ERROR)
125
+ raise
126
+
99
127
  self._current_filter = filter_name
100
128
  await self._change_motion_status(MotionStatus.POSITIONED)
101
129
  await self.comm.send_event(FilterChangedEvent(filter_name))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-fli"
3
- version = "2.0.0.dev11"
3
+ version = "2.0.0.dev12"
4
4
  description = "pyobs module for FLI cameras"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
@@ -73,7 +73,7 @@ async def test_run_blocking_runs_func_and_returns_true() -> None:
73
73
  def fast() -> None:
74
74
  ran.append(True)
75
75
 
76
- assert await FliCamera._run_blocking(fast) is True
76
+ assert await FliCamera()._run_blocking(fast) is True
77
77
  assert ran == [True]
78
78
 
79
79
 
@@ -84,7 +84,7 @@ async def test_run_blocking_times_out() -> None:
84
84
  def slow() -> None:
85
85
  done.wait()
86
86
 
87
- assert await FliCamera._run_blocking(slow, timeout=0.01) is False
87
+ assert await FliCamera()._run_blocking(slow, timeout=0.01) is False
88
88
  done.set()
89
89
  await asyncio.sleep(0.05)
90
90
 
@@ -61,14 +61,16 @@ async def test_set_filter_first_wheel() -> None:
61
61
  wheel = FliFilterWheel(filter_names=_TWO_WHEELS)
62
62
  wheel._driver = MagicMock()
63
63
  wheel._driver.set_filter_pos = MagicMock()
64
+ wheel._driver.get_filter_pos = MagicMock(return_value=0)
64
65
 
65
- async def fake_run(func, timeout: float = 5.0) -> None:
66
- func()
66
+ async def fake_run(func, timeout: float | None = None) -> object:
67
+ return func()
67
68
 
68
69
  wheel._run_blocking_or_raise = fake_run # type: ignore[method-assign]
69
70
 
70
71
  await wheel.set_filter("A")
71
72
  wheel._driver.set_filter_pos.assert_called_once_with(0)
73
+ wheel._driver.get_filter_pos.assert_called_once()
72
74
  assert wheel._current_filter == "A"
73
75
 
74
76
 
@@ -77,14 +79,16 @@ async def test_set_filter_second_wheel() -> None:
77
79
  wheel = FliFilterWheel(filter_names=_TWO_WHEELS)
78
80
  wheel._driver = MagicMock()
79
81
  wheel._driver.set_filter_pos = MagicMock()
82
+ wheel._driver.get_filter_pos = MagicMock(return_value=7)
80
83
 
81
- async def fake_run(func, timeout: float = 5.0) -> None:
82
- func()
84
+ async def fake_run(func, timeout: float | None = None) -> object:
85
+ return func()
83
86
 
84
87
  wheel._run_blocking_or_raise = fake_run # type: ignore[method-assign]
85
88
 
86
89
  await wheel.set_filter("H")
87
90
  wheel._driver.set_filter_pos.assert_called_once_with(7)
91
+ wheel._driver.get_filter_pos.assert_called_once()
88
92
  assert wheel._current_filter == "H"
89
93
 
90
94
 
@@ -2366,7 +2366,7 @@ gui = [
2366
2366
 
2367
2367
  [[package]]
2368
2368
  name = "pyobs-fli"
2369
- version = "2.0.0.dev11"
2369
+ version = "2.0.0.dev12"
2370
2370
  source = { editable = "." }
2371
2371
  dependencies = [
2372
2372
  { name = "astropy" },
File without changes