pyobs-brot 2.0.0.dev12__tar.gz → 2.0.0.dev14__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 (27) hide show
  1. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/.github/dependabot.yml +5 -0
  2. pyobs_brot-2.0.0.dev14/.github/workflows/pyrefly.yml +23 -0
  3. pyobs_brot-2.0.0.dev14/.github/workflows/pytest.yml +25 -0
  4. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/PKG-INFO +3 -3
  5. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/pyobs_brot/brotdome.py +4 -0
  6. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/pyobs_brot/brotroof.py +4 -0
  7. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/pyobs_brot/brottelescope.py +10 -2
  8. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/pyproject.toml +8 -3
  9. pyobs_brot-2.0.0.dev14/specs/index.md +17 -0
  10. pyobs_brot-2.0.0.dev14/tests/test_brotroof.py +34 -0
  11. pyobs_brot-2.0.0.dev14/tests/test_brottelescope.py +109 -0
  12. pyobs_brot-2.0.0.dev14/tests/test_import.py +49 -0
  13. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/uv.lock +1048 -787
  14. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/.github/workflows/pypi.yml +0 -0
  15. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/.github/workflows/ruff.yml +0 -0
  16. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/.gitignore +0 -0
  17. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/.pre-commit-config.yaml +0 -0
  18. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/.readthedocs.yml +0 -0
  19. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/LICENSE +0 -0
  20. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/README.md +0 -0
  21. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/docs/Makefile +0 -0
  22. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/docs/make.bat +0 -0
  23. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/docs/requirements.txt +0 -0
  24. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/docs/source/_static/pyobs.gif +0 -0
  25. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/docs/source/conf.py +0 -0
  26. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/docs/source/index.rst +0 -0
  27. {pyobs_brot-2.0.0.dev12 → pyobs_brot-2.0.0.dev14}/pyobs_brot/__init__.py +0 -0
@@ -6,5 +6,10 @@ updates:
6
6
  interval: "weekly"
7
7
  day: "tuesday"
8
8
  target-branch: "develop"
9
+ groups:
10
+ patch-and-minor:
11
+ update-types:
12
+ - "minor"
13
+ - "patch"
9
14
  labels:
10
15
  - "dependencies"
@@ -0,0 +1,23 @@
1
+ name: pyrefly
2
+ on: push
3
+
4
+ jobs:
5
+ pyrefly:
6
+ runs-on: ubuntu-latest
7
+ timeout-minutes: 10
8
+
9
+ steps:
10
+ - name: Check out repository code
11
+ uses: actions/checkout@v4
12
+
13
+ - name: Set up Python
14
+ uses: astral-sh/setup-uv@v5
15
+ with:
16
+ enable-cache: true
17
+ python-version: "3.13"
18
+
19
+ - name: Install packages
20
+ run: uv sync --all-extras --dev
21
+
22
+ - name: Run pyrefly
23
+ run: uv run pyrefly check
@@ -0,0 +1,25 @@
1
+ name: PyTest
2
+ on: push
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+ timeout-minutes: 10
8
+
9
+ steps:
10
+ - name: Check out repository code
11
+ uses: actions/checkout@v4
12
+
13
+ - name: Set up Python
14
+ uses: astral-sh/setup-uv@v5
15
+ with:
16
+ enable-cache: true
17
+ python-version: "3.13"
18
+
19
+ - name: Install packages
20
+ run: uv sync --all-extras --dev
21
+
22
+ - name: Run tests
23
+ run: uv run pytest tests
24
+ env:
25
+ CI: true
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pyobs-brot
3
- Version: 2.0.0.dev12
3
+ Version: 2.0.0.dev14
4
4
  Summary: pyobs module for BROTlib telescopes
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>, Lukas Melzig <lukas.melzig@stud.uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -9,4 +9,4 @@ Requires-Python: >=3.11
9
9
  Requires-Dist: astropy<8,>=7.0.1
10
10
  Requires-Dist: paho-mqtt>=2.1.0
11
11
  Requires-Dist: pybrotlib>=1.1.5
12
- Requires-Dist: pyobs-core<3,>=2.0.0.dev20
12
+ Requires-Dist: pyobs-core<3,>=2.0.0.dev48
@@ -76,6 +76,10 @@ class BrotDome(BaseDome, IDome):
76
76
 
77
77
  @timeout(300)
78
78
  async def init(self, **kwargs: Any) -> None:
79
+ # weather?
80
+ if not self.is_weather_good():
81
+ raise exc.InitError("Weather seems to be bad.")
82
+
79
83
  log.info("Opening roof")
80
84
  if self.brot.dome.shutter == DomeShutterStatus.OPEN:
81
85
  return
@@ -60,6 +60,10 @@ class BrotRoof(BaseRoof):
60
60
 
61
61
  @timeout(300)
62
62
  async def init(self, **kwargs: Any) -> None:
63
+ # weather?
64
+ if not self.is_weather_good():
65
+ raise exc.InitError("Weather seems to be bad.")
66
+
63
67
  log.info("Opening roof")
64
68
  if self.brot.roof.status == RoofStatus.OPEN:
65
69
  return
@@ -95,6 +95,10 @@ class BrotBaseTelescope(
95
95
  await self.comm.set_state(IOffsetsRaDec, RaDecOffsetState(ra=0.0, dec=0.0))
96
96
  await self.comm.set_state(IOffsetsAltAz, AltAzOffsetState(alt=0.0, az=0.0))
97
97
 
98
+ # placeholder -- real readings only exist once the MQTT client has had a chance to
99
+ # receive telemetry, which _update_task() (started above) hasn't had time to do yet
100
+ await self.comm.set_state(ITemperatures, TemperaturesState(readings=[]))
101
+
98
102
  async def close(self) -> None:
99
103
  await BaseTelescope.close(self)
100
104
  await self.mqtt.close()
@@ -238,6 +242,10 @@ class BrotBaseTelescope(
238
242
 
239
243
  @timeout(120)
240
244
  async def init(self, **kwargs: Any) -> None:
245
+ # weather?
246
+ if not self.is_weather_good():
247
+ raise exc.InitError("Weather seems to be bad.")
248
+
241
249
  match self.brot.telescope.status:
242
250
  case TelescopeStatus.PARKED:
243
251
  pass
@@ -324,8 +332,8 @@ class BrotRaDecTelescope(BrotBaseTelescope, IOffsetsRaDec):
324
332
  await self.brot.telescope.set_offset_dec(ddec * 3600)
325
333
  MAX_TARGET_DISTANCE = 2.0 / 3600.0
326
334
  while (
327
- self.brot.telescope._telemetry.POSITION.INSTRUMENTAL.HA.TARGETDISTANCE < MAX_TARGET_DISTANCE
328
- and self.brot.telescope._telemetry.POSITION.INSTRUMENTAL.DEC.TARGETDISTANCE < MAX_TARGET_DISTANCE
335
+ self.brot.telescope._telemetry.POSITION.INSTRUMENTAL.HA.TARGETDISTANCE > MAX_TARGET_DISTANCE
336
+ or self.brot.telescope._telemetry.POSITION.INSTRUMENTAL.DEC.TARGETDISTANCE > MAX_TARGET_DISTANCE
329
337
  ):
330
338
  await asyncio.sleep(0.1)
331
339
  await self.comm.set_state(IOffsetsRaDec, RaDecOffsetState(ra=dra, dec=ddec))
@@ -1,23 +1,25 @@
1
1
  [project]
2
2
  name = "pyobs-brot"
3
- version = "2.0.0.dev12"
3
+ version = "2.0.0.dev14"
4
4
  description = "pyobs module for BROTlib telescopes"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }, { name = "Lukas Melzig", email = "lukas.melzig@stud.uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
7
7
  license = "MIT"
8
8
  dependencies = [
9
9
  "astropy>=7.0.1,<8",
10
- "pyobs-core>=2.0.0.dev20,<3",
10
+ "pyobs-core>=2.0.0.dev48,<3",
11
11
  "paho-mqtt>=2.1.0",
12
12
  "pybrotlib>=1.1.5",
13
13
  ]
14
14
 
15
15
  [dependency-groups]
16
16
  dev = [
17
- "black>=23.7.0,<24",
17
+ "black>=23.7.0,<27",
18
18
  "ruff>=0.9.0",
19
19
  "pyrefly>=1.1.1",
20
20
  "pre-commit>=3.3.3,<5",
21
+ "pytest>=8.0.0",
22
+ "pytest-asyncio>=0.24.0",
21
23
  ]
22
24
 
23
25
  [build-system]
@@ -37,3 +39,6 @@ select = ["E", "F", "W", "I", "UP", "G"]
37
39
 
38
40
  [tool.pyrefly]
39
41
  python-version = "3.11"
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "strict"
@@ -0,0 +1,17 @@
1
+ # Design/planning docs
2
+
3
+ This repo has no `specs/` structure of its own. Design docs, implementation plans, and ADRs that
4
+ concern `pyobs-brot` — including ones actually implemented here — live in `pyobs-core`'s `specs/`
5
+ tree instead (`specs/design/`, `specs/plans/`, `specs/adrs/`), each tagged with a `Repos:` line
6
+ naming every repo it concerns. See `pyobs-core/CLAUDE.md`'s "Cross-repo docs" section.
7
+
8
+ Relevant so far:
9
+
10
+ - `pyobs-core/specs/plans/ejabberd-throughput-benchmarking.md` — XMPP/ejabberd throughput
11
+ benchmarking, including a production incident investigation involving `BrotDome`/
12
+ `BrotRaDecTelescope` on `pyobs-iag50` (disco#info capability fetches silently timing out after a
13
+ few modules restart together) and the `pybrotlib` `MQTTTransport.run()` event-loop-starvation fix
14
+ found along the way.
15
+ - `pyobs-core/specs/plans/enforce-state-publishing.md` — the state-publishing convention every
16
+ `Module` follows; `BrotRaDecTelescope`'s `ITemperatures` state had the same gap this doc's
17
+ convention exists to catch (2026-07-27 addendum).
@@ -0,0 +1,34 @@
1
+ """Unit tests for the roof-status -> motion-status mapping in BrotRoof.
2
+
3
+ The BROT/MQTT backend is mocked out; only the state mapping is exercised.
4
+ """
5
+
6
+ from unittest.mock import MagicMock
7
+
8
+ import pytest
9
+ from pybrotlib.components.roof import RoofStatus
10
+ from pyobs.utils.enums import MotionStatus
11
+
12
+ from pyobs_brot import BrotRoof
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ ("status", "expected"),
17
+ [
18
+ (RoofStatus.ERROR, MotionStatus.ERROR),
19
+ (RoofStatus.CLOSED, MotionStatus.PARKED),
20
+ (RoofStatus.OPENING, MotionStatus.INITIALIZING),
21
+ (RoofStatus.CLOSING, MotionStatus.PARKING),
22
+ (RoofStatus.OPEN, MotionStatus.POSITIONED),
23
+ (RoofStatus.STOPPED, MotionStatus.IDLE),
24
+ ],
25
+ )
26
+ @pytest.mark.asyncio
27
+ async def test_update_status_mapping(status: RoofStatus, expected: MotionStatus) -> None:
28
+ roof = BrotRoof(host="localhost", name="roof")
29
+ roof.brot.roof = MagicMock()
30
+ roof.brot.roof.status = status
31
+
32
+ await roof._update_status()
33
+
34
+ assert roof.motion_status() == expected
@@ -0,0 +1,109 @@
1
+ """Unit tests for the non-hardware logic in the BROT telescope drivers: constructor
2
+ state, focus-offset composition, and the RA/Dec offset scaling + convergence wait.
3
+
4
+ The BROT/MQTT backend (self.brot) is mocked out; hardware I/O itself is out of scope.
5
+ """
6
+
7
+ from types import SimpleNamespace
8
+ from typing import Any
9
+ from unittest.mock import AsyncMock, MagicMock
10
+
11
+ import pytest
12
+ from pyobs.interfaces import IFocuser, IOffsetsRaDec
13
+
14
+ from pyobs_brot import BrotRaDecTelescope
15
+
16
+
17
+ def test_constructor_defaults() -> None:
18
+ telescope = BrotRaDecTelescope(host="localhost", name="telescope")
19
+ assert telescope.temperatures == {}
20
+ assert telescope.focus_offset == 0.0
21
+ assert telescope._roof == "None"
22
+
23
+
24
+ def test_constructor_temperatures_and_roof() -> None:
25
+ telescope = BrotRaDecTelescope(
26
+ host="localhost",
27
+ name="telescope",
28
+ temperatures={"ccd": "T1"},
29
+ roof="some-roof",
30
+ )
31
+ assert telescope.temperatures == {"ccd": "T1"}
32
+ assert telescope._roof == "some-roof"
33
+
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_set_focus_offset_composes_and_publishes() -> None:
37
+ telescope = BrotRaDecTelescope(host="localhost", name="telescope")
38
+ telescope.brot.focus = MagicMock()
39
+ telescope.brot.focus.position = 10.0
40
+ telescope.brot.focus.set = AsyncMock()
41
+ telescope._wait_for_focus = AsyncMock() # type: ignore[method-assign]
42
+ telescope.comm.set_state = AsyncMock() # type: ignore[method-assign]
43
+
44
+ await telescope.set_focus_offset(0.25)
45
+
46
+ # the hardware call adds the offset onto the current position...
47
+ telescope.brot.focus.set.assert_awaited_once_with(10.25)
48
+ assert telescope.focus_offset == 0.25
49
+
50
+ # ...and the published focus subtracts the offset back out of the new position
51
+ assert telescope.comm.set_state.await_args is not None
52
+ interface, state = telescope.comm.set_state.await_args.args
53
+ assert interface is IFocuser
54
+ assert (state.focus, state.focus_offset) == (9.75, 0.25)
55
+
56
+
57
+ def _telemetry(ha_dist: float, dec_dist: float) -> Any:
58
+ return SimpleNamespace(
59
+ POSITION=SimpleNamespace(
60
+ INSTRUMENTAL=SimpleNamespace(
61
+ HA=SimpleNamespace(TARGETDISTANCE=ha_dist),
62
+ DEC=SimpleNamespace(TARGETDISTANCE=dec_dist),
63
+ )
64
+ )
65
+ )
66
+
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_set_offsets_radec_sends_scaled_and_sign_flipped_offsets() -> None:
70
+ telescope = BrotRaDecTelescope(host="localhost", name="telescope")
71
+ telescope.brot.telescope.set_offset_ha = AsyncMock() # type: ignore[method-assign]
72
+ telescope.brot.telescope.set_offset_dec = AsyncMock() # type: ignore[method-assign]
73
+ telescope.brot.telescope._telemetry = _telemetry(0.0, 0.0) # already converged
74
+ telescope.comm.set_state = AsyncMock() # type: ignore[method-assign]
75
+
76
+ await telescope.set_offsets_radec(0.001, 0.002)
77
+
78
+ # HA is sign-flipped (RA east is HA west) and both are scaled deg -> arcsec
79
+ telescope.brot.telescope.set_offset_ha.assert_awaited_once_with(-3.6)
80
+ telescope.brot.telescope.set_offset_dec.assert_awaited_once_with(7.2)
81
+ assert telescope.comm.set_state.await_args is not None
82
+ interface, state = telescope.comm.set_state.await_args.args
83
+ assert interface is IOffsetsRaDec
84
+ assert (state.ra, state.dec) == (0.001, 0.002)
85
+
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_set_offsets_radec_waits_until_both_axes_converge(monkeypatch: pytest.MonkeyPatch) -> None:
89
+ telescope = BrotRaDecTelescope(host="localhost", name="telescope")
90
+ telescope.brot.telescope.set_offset_ha = AsyncMock() # type: ignore[method-assign]
91
+ telescope.brot.telescope.set_offset_dec = AsyncMock() # type: ignore[method-assign]
92
+ telescope.comm.set_state = AsyncMock() # type: ignore[method-assign]
93
+
94
+ # still slewing for two polls (HA, then DEC, still off-target), converged on the third
95
+ readings = [(1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
96
+ telescope.brot.telescope._telemetry = _telemetry(*readings[0])
97
+
98
+ sleep_calls = 0
99
+
100
+ async def fake_sleep(_seconds: float) -> None:
101
+ nonlocal sleep_calls
102
+ sleep_calls += 1
103
+ telescope.brot.telescope._telemetry = _telemetry(*readings[sleep_calls])
104
+
105
+ monkeypatch.setattr("pyobs_brot.brottelescope.asyncio.sleep", fake_sleep)
106
+
107
+ await telescope.set_offsets_radec(0.001, 0.002)
108
+
109
+ assert sleep_calls == 2
@@ -0,0 +1,49 @@
1
+ """Smoke tests: import every public module and instantiate each driver class without
2
+ hardware, asserting the interfaces they claim.
3
+
4
+ pybrotlib is a pure-Python MQTT client that connects at connect() time, not at
5
+ construction, so instantiation is safe with no broker running.
6
+ """
7
+
8
+ from pyobs.interfaces import (
9
+ IDome,
10
+ IFocuser,
11
+ IOffsetsAltAz,
12
+ IOffsetsRaDec,
13
+ IPointingAltAz,
14
+ IPointingRaDec,
15
+ IRoof,
16
+ ITemperatures,
17
+ )
18
+ from pyobs.modules import Module
19
+
20
+ from pyobs_brot import BrotDome, BrotRaDecTelescope, BrotRoof
21
+ from pyobs_brot.brottelescope import BrotAltAzTelescope
22
+
23
+
24
+ def test_instantiate_dome() -> None:
25
+ dome = BrotDome(host="localhost", name="dome")
26
+ assert isinstance(dome, Module)
27
+ assert isinstance(dome, IDome)
28
+
29
+
30
+ def test_instantiate_roof() -> None:
31
+ roof = BrotRoof(host="localhost", name="roof")
32
+ assert isinstance(roof, Module)
33
+ assert isinstance(roof, IRoof)
34
+
35
+
36
+ def test_instantiate_radec_telescope() -> None:
37
+ telescope = BrotRaDecTelescope(host="localhost", name="telescope")
38
+ assert isinstance(telescope, Module)
39
+ assert isinstance(telescope, IFocuser)
40
+ assert isinstance(telescope, ITemperatures)
41
+ assert isinstance(telescope, IPointingRaDec)
42
+ assert isinstance(telescope, IPointingAltAz)
43
+ assert isinstance(telescope, IOffsetsRaDec)
44
+
45
+
46
+ def test_instantiate_altaz_telescope() -> None:
47
+ telescope = BrotAltAzTelescope(host="localhost", name="telescope")
48
+ assert isinstance(telescope, Module)
49
+ assert isinstance(telescope, IOffsetsAltAz)