pyobs-alpaca 2.0.0.dev3__tar.gz → 2.0.0.dev5__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 (30) hide show
  1. pyobs_alpaca-2.0.0.dev5/.flake8 +14 -0
  2. pyobs_alpaca-2.0.0.dev5/.github/dependabot.yml +15 -0
  3. pyobs_alpaca-2.0.0.dev5/.github/workflows/dependabot-automerge.yml +24 -0
  4. pyobs_alpaca-2.0.0.dev5/.github/workflows/pyrefly.yml +23 -0
  5. pyobs_alpaca-2.0.0.dev5/.github/workflows/pytest.yml +25 -0
  6. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/PKG-INFO +3 -3
  7. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/pyobs_alpaca/device.py +13 -0
  8. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/pyobs_alpaca/dome.py +14 -9
  9. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/pyobs_alpaca/focuser.py +13 -11
  10. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/pyobs_alpaca/telescope.py +16 -9
  11. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/pyproject.toml +8 -3
  12. pyobs_alpaca-2.0.0.dev5/tests/test_cooperative_init.py +64 -0
  13. pyobs_alpaca-2.0.0.dev5/tests/test_device.py +68 -0
  14. pyobs_alpaca-2.0.0.dev5/tests/test_import.py +47 -0
  15. pyobs_alpaca-2.0.0.dev5/uv.lock +2604 -0
  16. pyobs_alpaca-2.0.0.dev3/uv.lock +0 -2107
  17. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/.github/workflows/pypi.yml +0 -0
  18. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/.github/workflows/ruff.yml +0 -0
  19. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/.pre-commit-config.yaml +0 -0
  20. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/.readthedocs.yml +0 -0
  21. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/CHANGELOG.rst +0 -0
  22. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/LICENSE +0 -0
  23. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/README.md +0 -0
  24. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/docs/Makefile +0 -0
  25. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/docs/make.bat +0 -0
  26. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/docs/requirements.txt +0 -0
  27. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/docs/source/_static/pyobs.gif +0 -0
  28. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/docs/source/conf.py +0 -0
  29. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/docs/source/index.rst +0 -0
  30. {pyobs_alpaca-2.0.0.dev3 → pyobs_alpaca-2.0.0.dev5}/pyobs_alpaca/__init__.py +0 -0
@@ -0,0 +1,14 @@
1
+ [flake8]
2
+ ignore =
3
+ E203, E266, E501, W503, F403,
4
+ # E704: Multiple statements on one line (def)
5
+ E704,
6
+ # E722: do not use bare 'except'
7
+ E722
8
+ # E701: multiple statements on one line (colon)
9
+ E701
10
+ per-file-ignores = __init__.py:F401
11
+ max-line-length = 120
12
+ max-complexity = 30
13
+ select = B, C, E, F, W, T4, B9
14
+ exclude = .git,.venv,__pycache__,docs,build,dist
@@ -0,0 +1,15 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "uv"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ day: "tuesday"
8
+ target-branch: "develop"
9
+ groups:
10
+ patch-and-minor:
11
+ update-types:
12
+ - "minor"
13
+ - "patch"
14
+ labels:
15
+ - "dependencies"
@@ -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 }}
@@ -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,10 +1,10 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pyobs-alpaca
3
- Version: 2.0.0.dev3
3
+ Version: 2.0.0.dev5
4
4
  Summary: pyobs module for ASCOM Alpaca
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.11
9
9
  Requires-Dist: numpy<3,>=2.2.5
10
- Requires-Dist: pyobs-core<3,>=2.0.0.dev1
10
+ Requires-Dist: pyobs-core<3,>=2.0.0.dev82
@@ -23,6 +23,19 @@ class ServerGetResponse(NamedTuple):
23
23
  Value: Any
24
24
 
25
25
 
26
+ DEVICE_INIT_KWARGS = {"server", "port", "device_type", "device", "version", "alive_parameter"}
27
+ """Names of AlpacaDevice's own constructor params -- callers that also thread **kwargs through
28
+ their own cooperative super().__init__() chain must exclude these first, since nothing in that
29
+ chain (Module/mixins) recognizes them and they'd otherwise leak through to object.__init__()."""
30
+
31
+ OBJECT_SHARED_KWARGS = {"vfs", "comm", "timezone", "location", "observer"}
32
+ """Names of pyobs.object.Object's own constructor params. AlpacaDevice forwards anything beyond
33
+ its own params (see DEVICE_INIT_KWARGS) straight to Object cooperatively, so callers building an
34
+ AlpacaDevice from a module's **kwargs must restrict to DEVICE_INIT_KWARGS | OBJECT_SHARED_KWARGS --
35
+ any module/mixin-specific kwarg (e.g. fits_namespaces, motion_status_interfaces) would otherwise
36
+ leak through to object.__init__() the same way."""
37
+
38
+
26
39
  class AlpacaDevice(Object):
27
40
  def __init__(
28
41
  self,
@@ -13,12 +13,12 @@ from pyobs.utils.enums import MotionStatus
13
13
  from pyobs.utils.parallel import event_wait
14
14
  from pyobs.utils.threads import LockWithAbort
15
15
 
16
- from .device import AlpacaDevice
16
+ from .device import DEVICE_INIT_KWARGS, OBJECT_SHARED_KWARGS, AlpacaDevice
17
17
 
18
18
  log = logging.getLogger("pyobs")
19
19
 
20
20
 
21
- class AlpacaDome(FollowMixin, BaseDome):
21
+ class AlpacaDome(BaseDome, FollowMixin):
22
22
  __module__ = "pyobs_alpaca"
23
23
 
24
24
  def __init__(self, tolerance: float = 3, park_az: float = 180, follow: str | None = None, **kwargs: Any):
@@ -29,10 +29,20 @@ class AlpacaDome(FollowMixin, BaseDome):
29
29
  park_az: Azimuth for park position.
30
30
  follow: Name of other device (e.g. telescope) to follow.
31
31
  """
32
- BaseDome.__init__(self, **kwargs, motion_status_interfaces=["IDome"])
32
+ super().__init__(
33
+ motion_status_interfaces=["IDome"],
34
+ device=follow,
35
+ interval=10,
36
+ tolerance=tolerance,
37
+ mode=IPointingAltAz,
38
+ only_follow_when_ready=False,
39
+ **{k: v for k, v in kwargs.items() if k not in DEVICE_INIT_KWARGS},
40
+ )
33
41
 
34
42
  # device
35
- self._device = self.add_child_object(AlpacaDevice, **kwargs)
43
+ self._device = self.add_child_object(
44
+ AlpacaDevice, **{k: v for k, v in kwargs.items() if k in DEVICE_INIT_KWARGS | OBJECT_SHARED_KWARGS}
45
+ )
36
46
 
37
47
  # store
38
48
  self._tolerance = tolerance
@@ -53,11 +63,6 @@ class AlpacaDome(FollowMixin, BaseDome):
53
63
  # start thread
54
64
  self.add_background_task(self._update_status)
55
65
 
56
- # mixins
57
- FollowMixin.__init__(
58
- self, device=follow, interval=10, tolerance=tolerance, mode=IPointingAltAz, only_follow_when_ready=False
59
- )
60
-
61
66
  async def open(self) -> None:
62
67
  """Open module."""
63
68
  await BaseDome.open(self)
@@ -2,26 +2,31 @@ import asyncio
2
2
  import logging
3
3
  from typing import Any
4
4
 
5
- from pyobs.interfaces import FocuserState, IFitsHeaderBefore, IFocuser, IReady, ReadyState
5
+ from pyobs.interfaces import FitsHeaderEntry, FocuserState, IFitsHeaderBefore, IFocuser, IReady, ReadyState
6
6
  from pyobs.mixins import MotionStatusMixin
7
7
  from pyobs.modules import Module, timeout
8
8
  from pyobs.utils import exceptions as exc
9
9
  from pyobs.utils.enums import MotionStatus
10
10
  from pyobs.utils.threads import LockWithAbort
11
11
 
12
- from .device import AlpacaDevice
12
+ from .device import DEVICE_INIT_KWARGS, OBJECT_SHARED_KWARGS, AlpacaDevice
13
13
 
14
14
  log = logging.getLogger(__name__)
15
15
 
16
16
 
17
- class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
17
+ class AlpacaFocuser(Module, MotionStatusMixin, IFocuser, IFitsHeaderBefore):
18
18
  __module__ = "pyobs_alpaca"
19
19
 
20
20
  def __init__(self, **kwargs: Any):
21
- Module.__init__(self, **kwargs)
21
+ super().__init__(
22
+ motion_status_interfaces=["IFocuser"],
23
+ **{k: v for k, v in kwargs.items() if k not in DEVICE_INIT_KWARGS},
24
+ )
22
25
 
23
26
  # device
24
- self._device = self.add_child_object(AlpacaDevice, **kwargs)
27
+ self._device = self.add_child_object(
28
+ AlpacaDevice, **{k: v for k, v in kwargs.items() if k in DEVICE_INIT_KWARGS | OBJECT_SHARED_KWARGS}
29
+ )
25
30
 
26
31
  # variables
27
32
  self._focus_offset = 0.0
@@ -30,11 +35,8 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
30
35
  self._lock_motion = asyncio.Lock()
31
36
  self._abort_motion = asyncio.Event()
32
37
 
33
- # init mixins
34
- MotionStatusMixin.__init__(self, motion_status_interfaces=["IFocuser"])
35
-
36
38
  # register exception
37
- exc.register_exception(exc.MotionError, 3, timespan=600, callback=self._default_remote_error_callback)
39
+ self._register_exception(exc.MotionError, 3, timespan=600, callback=self._default_remote_error_callback)
38
40
 
39
41
  async def open(self) -> None:
40
42
  """Open module."""
@@ -57,13 +59,13 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
57
59
 
58
60
  async def get_fits_header_before(
59
61
  self, namespaces: list[str] | None = None, **kwargs: Any
60
- ) -> dict[str, tuple[Any, str]]:
62
+ ) -> dict[str, FitsHeaderEntry]:
61
63
  """Returns FITS header for the current status of this module."""
62
64
 
63
65
  try:
64
66
  pos = await self._device.get("Position")
65
67
  step = await self._device.get("StepSize") * 1000.0
66
- return {"TEL-FOCU": (pos / step, "Focus of telescope [mm]")}
68
+ return {"TEL-FOCU": FitsHeaderEntry(pos / step, "Focus of telescope [mm]")}
67
69
  except ConnectionError as e:
68
70
  log.warning("Could not determine focus position: %s", e)
69
71
  return {}
@@ -6,6 +6,7 @@ import numpy as np
6
6
  from pyobs.events import OffsetsRaDecEvent
7
7
  from pyobs.interfaces import (
8
8
  AltAzState,
9
+ FitsHeaderEntry,
9
10
  IFitsHeaderBefore,
10
11
  IOffsetsRaDec,
11
12
  IPointingAltAz,
@@ -24,7 +25,7 @@ from pyobs.utils.enums import MotionStatus
24
25
  from pyobs.utils.parallel import acquire_lock, event_wait
25
26
  from pyobs.utils.threads import LockWithAbort
26
27
 
27
- from .device import AlpacaDevice
28
+ from .device import DEVICE_INIT_KWARGS, OBJECT_SHARED_KWARGS, AlpacaDevice
28
29
 
29
30
  log = logging.getLogger("pyobs")
30
31
 
@@ -39,10 +40,15 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
39
40
  settle_time: Time in seconds to wait after slew before finishing.
40
41
  park_position: Alt/Az park position.
41
42
  """
42
- BaseTelescope.__init__(self, **kwargs, motion_status_interfaces=["ITelescope"])
43
+ super().__init__(
44
+ motion_status_interfaces=["ITelescope"],
45
+ **{k: v for k, v in kwargs.items() if k not in DEVICE_INIT_KWARGS},
46
+ )
43
47
 
44
48
  # device
45
- self._device = self.add_child_object(AlpacaDevice, **kwargs)
49
+ self._device = self.add_child_object(
50
+ AlpacaDevice, **{k: v for k, v in kwargs.items() if k in DEVICE_INIT_KWARGS | OBJECT_SHARED_KWARGS}
51
+ )
46
52
 
47
53
  # variables
48
54
  self._settle_time = settle_time
@@ -56,9 +62,6 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
56
62
  self._cached_ra: float | None = None
57
63
  self._cached_dec: float | None = None
58
64
 
59
- # mixins
60
- FitsNamespaceMixin.__init__(self, **kwargs)
61
-
62
65
  # background position polling
63
66
  self.add_background_task(self._update_position)
64
67
 
@@ -219,6 +222,10 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
219
222
  await self.stop_motion()
220
223
  raise exc.MoveError("Could not move telescope to RA/Dec.")
221
224
 
225
+ async def _set_tracking_rate(self, ra_rate: float, dec_rate: float) -> None:
226
+ """Tracking rates are not supported by the Alpaca telescope driver."""
227
+ raise NotImplementedError
228
+
222
229
  @timeout(10000)
223
230
  async def set_offsets_radec(self, dra: float, ddec: float, **kwargs: Any) -> None:
224
231
  """Move an RA/Dec offset."""
@@ -295,14 +302,14 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
295
302
 
296
303
  async def get_fits_header_before(
297
304
  self, namespaces: list[str] | None = None, **kwargs: Any
298
- ) -> dict[str, tuple[Any, str]]:
305
+ ) -> dict[str, FitsHeaderEntry]:
299
306
  """Returns FITS header for the current status of this module."""
300
307
 
301
308
  hdr = await BaseTelescope.get_fits_header_before(self)
302
309
 
303
310
  try:
304
- hdr["RAOFF"] = (self._offset_ra, "RA offset [deg]")
305
- hdr["DECOFF"] = (self._offset_dec, "Dec offset [deg]")
311
+ hdr["RAOFF"] = FitsHeaderEntry(self._offset_ra, "RA offset [deg]")
312
+ hdr["DECOFF"] = FitsHeaderEntry(self._offset_dec, "Dec offset [deg]")
306
313
  return self._filter_fits_namespace(hdr, namespaces=namespaces, **kwargs)
307
314
  except ConnectionError:
308
315
  return {}
@@ -1,21 +1,23 @@
1
1
  [project]
2
2
  name = "pyobs-alpaca"
3
- version = "2.0.0.dev3"
3
+ version = "2.0.0.dev5"
4
4
  description = "pyobs module for ASCOM Alpaca"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
7
7
  license = "MIT"
8
8
  dependencies = [
9
9
  "numpy>=2.2.5,<3",
10
- "pyobs-core>=2.0.0.dev1,<3",
10
+ "pyobs-core>=2.0.0.dev82,<3",
11
11
  ]
12
12
 
13
13
  [dependency-groups]
14
14
  dev = [
15
- "black>=25.1.0,<26",
15
+ "black>=25.1.0,<27",
16
16
  "ruff>=0.9.0",
17
17
  "pyrefly>=1.1.1",
18
18
  "pre-commit>=4.2.0,<5",
19
+ "pytest>=8.0.0",
20
+ "pytest-asyncio>=0.24.0",
19
21
  ]
20
22
 
21
23
  [build-system]
@@ -35,3 +37,6 @@ select = ["E", "F", "W", "I", "UP", "G"]
35
37
 
36
38
  [tool.pyrefly]
37
39
  python-version = "3.11"
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "strict"
@@ -0,0 +1,64 @@
1
+ """Regression tests for the cooperative-super()-chain conversion of AlpacaDome, AlpacaFocuser,
2
+ and AlpacaTelescope.
3
+
4
+ Each of these classes used to make separate, explicit calls to its Module/Base and mixin
5
+ __init__ methods with unfiltered kwargs, instead of a single cooperative super().__init__()
6
+ call. Against pyobs-core's cooperative __init__ chains, that pattern either drops kwargs a
7
+ sibling mixin declares (leaking them to object.__init__()) or re-threads already-consumed
8
+ kwargs through a second, redundant call -- both raise TypeError. If that happens here, these
9
+ constructor calls themselves raise instead of returning.
10
+ """
11
+
12
+ import inspect
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from pyobs.mixins.follow import FollowMixin
17
+ from pyobs.object import Object
18
+ from pyobs.utils.enums import MotionStatus
19
+
20
+ from pyobs_alpaca import AlpacaDome, AlpacaFocuser, AlpacaTelescope
21
+ from pyobs_alpaca.device import DEVICE_INIT_KWARGS, OBJECT_SHARED_KWARGS, AlpacaDevice
22
+
23
+ _DEVICE_ARGS = dict(server="localhost", port=11111, device_type="dome", device=0)
24
+
25
+
26
+ def _param_names(func: Callable[..., Any]) -> set[str]:
27
+ return {name for name in inspect.signature(func).parameters if name not in ("self", "kwargs")}
28
+
29
+
30
+ def test_device_init_kwargs_matches_alpaca_device_signature() -> None:
31
+ """Keeps DEVICE_INIT_KWARGS honest: if AlpacaDevice's constructor ever gains/loses a param
32
+ without updating this set, callers threading a module's **kwargs through both AlpacaDevice
33
+ and the module's own super().__init__() chain would silently start leaking kwargs to
34
+ object.__init__() again -- exactly the bug this whole conversion fixes."""
35
+ assert DEVICE_INIT_KWARGS == _param_names(AlpacaDevice.__init__)
36
+
37
+
38
+ def test_object_shared_kwargs_matches_object_signature() -> None:
39
+ """Same invariant as above, for the Object-level kwargs AlpacaDevice forwards cooperatively
40
+ (and that the module chain also needs, e.g. comm/vfs)."""
41
+ assert OBJECT_SHARED_KWARGS == _param_names(Object.__init__)
42
+
43
+
44
+ def test_dome_constructor_threads_kwargs_cooperatively() -> None:
45
+ dome = AlpacaDome(tolerance=5, follow="telescope", **_DEVICE_ARGS)
46
+ assert dome.motion_status() == MotionStatus.UNKNOWN
47
+ assert isinstance(dome, FollowMixin)
48
+ assert dome.is_following is True
49
+ assert dome._tolerance == 5
50
+
51
+
52
+ def test_focuser_constructor_threads_kwargs_cooperatively() -> None:
53
+ focuser = AlpacaFocuser(**{**_DEVICE_ARGS, "device_type": "focuser"})
54
+ assert focuser.motion_status() == MotionStatus.UNKNOWN
55
+
56
+
57
+ def test_telescope_constructor_threads_kwargs_cooperatively() -> None:
58
+ telescope = AlpacaTelescope(
59
+ settle_time=1.5,
60
+ fits_namespaces={"instrument": None},
61
+ **{**_DEVICE_ARGS, "device_type": "telescope"},
62
+ )
63
+ assert telescope.motion_status() == MotionStatus.UNKNOWN
64
+ assert telescope._FitsNamespaceMixin__namespaces == {"instrument": None} # type: ignore[attr-defined]
@@ -0,0 +1,68 @@
1
+ """Unit tests for the pure, non-hardware logic in AlpacaDevice.
2
+
3
+ Hardware I/O (open/close, talking to the ASCOM server) is out of scope here; these cover
4
+ URL construction, version validation, the connection guard, and the connected-state
5
+ transition logic.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ import pytest
11
+
12
+ from pyobs_alpaca.device import AlpacaDevice
13
+
14
+ _ARGS = dict(server="myserver", port=11111, device_type="telescope", device=3)
15
+
16
+
17
+ def test_build_alpaca_url() -> None:
18
+ device = AlpacaDevice(**_ARGS)
19
+ assert device._build_alpaca_url("RightAscension") == ("http://myserver:11111/api/v1/telescope/3/rightascension")
20
+
21
+
22
+ def test_only_v1_supported() -> None:
23
+ with pytest.raises(ValueError):
24
+ AlpacaDevice(**_ARGS, version="v2")
25
+
26
+
27
+ def test_starts_disconnected() -> None:
28
+ device = AlpacaDevice(**_ARGS)
29
+ assert device.connected is False
30
+
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_get_requires_connection() -> None:
34
+ device = AlpacaDevice(**_ARGS)
35
+ with pytest.raises(ConnectionError):
36
+ await device.get("RightAscension")
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_put_requires_connection() -> None:
41
+ device = AlpacaDevice(**_ARGS)
42
+ with pytest.raises(ConnectionError):
43
+ await device.put("Tracking", Tracking=False)
44
+
45
+
46
+ @pytest.mark.asyncio
47
+ async def test_check_connected_sets_flag_on_success(monkeypatch: pytest.MonkeyPatch) -> None:
48
+ device = AlpacaDevice(**_ARGS)
49
+
50
+ async def fake_get(name: str) -> Any:
51
+ return True
52
+
53
+ monkeypatch.setattr(device, "_get", fake_get)
54
+ await device._check_connected()
55
+ assert device.connected is True
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_check_connected_clears_flag_on_connection_error(monkeypatch: pytest.MonkeyPatch) -> None:
60
+ device = AlpacaDevice(**_ARGS)
61
+ device._connected = True
62
+
63
+ async def fake_get(name: str) -> Any:
64
+ raise ConnectionError
65
+
66
+ monkeypatch.setattr(device, "_get", fake_get)
67
+ await device._check_connected()
68
+ assert device.connected is False
@@ -0,0 +1,47 @@
1
+ """Smoke tests: import every public module and instantiate each driver class with a
2
+ missing/mocked hardware connection, asserting it loads and advertises the interfaces it
3
+ claims.
4
+
5
+ No real hardware or ASCOM server is involved: instantiation only registers background
6
+ tasks, which never start until open() is called.
7
+ """
8
+
9
+ from pyobs.interfaces import (
10
+ IDome,
11
+ IFitsHeaderBefore,
12
+ IFocuser,
13
+ IOffsetsRaDec,
14
+ ISyncTarget,
15
+ ITelescope,
16
+ )
17
+ from pyobs.object import Object
18
+
19
+ from pyobs_alpaca import AlpacaDome, AlpacaFocuser, AlpacaTelescope
20
+ from pyobs_alpaca.device import AlpacaDevice
21
+
22
+ _TELESCOPE_ARGS = dict(server="localhost", port=11111, device_type="telescope", device=0)
23
+
24
+
25
+ def test_import_device_and_instantiate() -> None:
26
+ device = AlpacaDevice(**_TELESCOPE_ARGS)
27
+ assert isinstance(device, Object)
28
+ assert not device.connected
29
+
30
+
31
+ def test_import_telescope_and_instantiate() -> None:
32
+ telescope = AlpacaTelescope(**_TELESCOPE_ARGS)
33
+ assert isinstance(telescope, ITelescope)
34
+ assert isinstance(telescope, IFitsHeaderBefore)
35
+ assert isinstance(telescope, IOffsetsRaDec)
36
+ assert isinstance(telescope, ISyncTarget)
37
+
38
+
39
+ def test_import_focuser_and_instantiate() -> None:
40
+ focuser = AlpacaFocuser(server="localhost", port=11111, device_type="focuser", device=0)
41
+ assert isinstance(focuser, IFocuser)
42
+ assert isinstance(focuser, IFitsHeaderBefore)
43
+
44
+
45
+ def test_import_dome_and_instantiate() -> None:
46
+ dome = AlpacaDome(server="localhost", port=11111, device_type="dome", device=0)
47
+ assert isinstance(dome, IDome)