pyobs-alpaca 2.0.0.dev2__tar.gz → 2.0.0.dev4__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 (29) hide show
  1. pyobs_alpaca-2.0.0.dev4/.flake8 +14 -0
  2. pyobs_alpaca-2.0.0.dev4/.github/dependabot.yml +15 -0
  3. pyobs_alpaca-2.0.0.dev4/.github/workflows/pyrefly.yml +23 -0
  4. pyobs_alpaca-2.0.0.dev4/.github/workflows/pytest.yml +25 -0
  5. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.readthedocs.yml +1 -0
  6. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/PKG-INFO +3 -3
  7. pyobs_alpaca-2.0.0.dev4/README.md +65 -0
  8. pyobs_alpaca-2.0.0.dev4/docs/requirements.txt +1 -0
  9. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/focuser.py +4 -4
  10. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/telescope.py +8 -3
  11. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyproject.toml +8 -3
  12. pyobs_alpaca-2.0.0.dev4/tests/test_device.py +68 -0
  13. pyobs_alpaca-2.0.0.dev4/tests/test_import.py +47 -0
  14. pyobs_alpaca-2.0.0.dev4/uv.lock +2604 -0
  15. pyobs_alpaca-2.0.0.dev2/README.md +0 -6
  16. pyobs_alpaca-2.0.0.dev2/uv.lock +0 -2107
  17. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.github/workflows/pypi.yml +0 -0
  18. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.github/workflows/ruff.yml +0 -0
  19. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.pre-commit-config.yaml +0 -0
  20. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/CHANGELOG.rst +0 -0
  21. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/LICENSE +0 -0
  22. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/Makefile +0 -0
  23. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/make.bat +0 -0
  24. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/source/_static/pyobs.gif +0 -0
  25. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/source/conf.py +0 -0
  26. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/source/index.rst +0 -0
  27. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/__init__.py +0 -0
  28. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/device.py +0 -0
  29. {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/dome.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,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
@@ -12,3 +12,4 @@ python:
12
12
  install:
13
13
  - method: pip
14
14
  path: .
15
+ - requirements: docs/requirements.txt
@@ -1,10 +1,10 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pyobs-alpaca
3
- Version: 2.0.0.dev2
3
+ Version: 2.0.0.dev4
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.dev48
@@ -0,0 +1,65 @@
1
+ ASCOM Alpaca wrapper for *pyobs*
2
+ ================================
3
+
4
+ This is a [pyobs](https://www.pyobs.org) module for wrapping devices accessible via
5
+ [ASCOM Alpaca](https://ascom-standards.org/AlpacaDeveloper/), a HTTP proxy for ASCOM. It provides
6
+ telescope, focuser and dome implementations.
7
+
8
+
9
+ Install *pyobs-alpaca*
10
+ -----------------------
11
+ Clone the repository:
12
+
13
+ git clone https://github.com/pyobs/pyobs-alpaca.git
14
+ cd pyobs-alpaca
15
+
16
+ Install it with [uv](https://docs.astral.sh/uv/):
17
+
18
+ uv sync
19
+
20
+ Alternatively, with plain `venv`/`pip`:
21
+
22
+ python3 -m venv .venv
23
+ source .venv/bin/activate
24
+ pip install .
25
+
26
+
27
+ Configuration
28
+ -------------
29
+ This is an example configuration for a telescope:
30
+
31
+ class: pyobs_alpaca.AlpacaTelescope
32
+ name: ASCOM Telescope
33
+ server: 1.2.3.4
34
+ port: 11111
35
+ device_type: telescope
36
+ device: 0
37
+ wait_for_dome: dome
38
+ weather: weather
39
+ alive_parameter: Name
40
+
41
+ And for a focusing unit:
42
+
43
+ class: pyobs_alpaca.AlpacaFocuser
44
+ name: ASCOM Focuser
45
+ server: 1.2.3.4
46
+ port: 11111
47
+ device_type: focuser
48
+ device: 0
49
+ alive_parameter: Position
50
+
51
+ And finally, for a dome:
52
+
53
+ class: pyobs_alpaca.AlpacaDome
54
+ server: 1.2.3.4
55
+ port: 11111
56
+ device_type: dome
57
+ device: 0
58
+ follow: telescope
59
+ weather: weather
60
+
61
+
62
+ Dependencies
63
+ ------------
64
+ * [pyobs-core](https://github.com/pyobs/pyobs-core) for the core functionality.
65
+ * [NumPy](https://numpy.org/) for array handling.
@@ -0,0 +1 @@
1
+ sphinx-rtd-theme
@@ -2,7 +2,7 @@ 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
@@ -34,7 +34,7 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
34
34
  MotionStatusMixin.__init__(self, motion_status_interfaces=["IFocuser"])
35
35
 
36
36
  # register exception
37
- exc.register_exception(exc.MotionError, 3, timespan=600, callback=self._default_remote_error_callback)
37
+ self._register_exception(exc.MotionError, 3, timespan=600, callback=self._default_remote_error_callback)
38
38
 
39
39
  async def open(self) -> None:
40
40
  """Open module."""
@@ -57,13 +57,13 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
57
57
 
58
58
  async def get_fits_header_before(
59
59
  self, namespaces: list[str] | None = None, **kwargs: Any
60
- ) -> dict[str, tuple[Any, str]]:
60
+ ) -> dict[str, FitsHeaderEntry]:
61
61
  """Returns FITS header for the current status of this module."""
62
62
 
63
63
  try:
64
64
  pos = await self._device.get("Position")
65
65
  step = await self._device.get("StepSize") * 1000.0
66
- return {"TEL-FOCU": (pos / step, "Focus of telescope [mm]")}
66
+ return {"TEL-FOCU": FitsHeaderEntry(pos / step, "Focus of telescope [mm]")}
67
67
  except ConnectionError as e:
68
68
  log.warning("Could not determine focus position: %s", e)
69
69
  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,
@@ -219,6 +220,10 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
219
220
  await self.stop_motion()
220
221
  raise exc.MoveError("Could not move telescope to RA/Dec.")
221
222
 
223
+ async def _set_tracking_rate(self, ra_rate: float, dec_rate: float) -> None:
224
+ """Tracking rates are not supported by the Alpaca telescope driver."""
225
+ raise NotImplementedError
226
+
222
227
  @timeout(10000)
223
228
  async def set_offsets_radec(self, dra: float, ddec: float, **kwargs: Any) -> None:
224
229
  """Move an RA/Dec offset."""
@@ -295,14 +300,14 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
295
300
 
296
301
  async def get_fits_header_before(
297
302
  self, namespaces: list[str] | None = None, **kwargs: Any
298
- ) -> dict[str, tuple[Any, str]]:
303
+ ) -> dict[str, FitsHeaderEntry]:
299
304
  """Returns FITS header for the current status of this module."""
300
305
 
301
306
  hdr = await BaseTelescope.get_fits_header_before(self)
302
307
 
303
308
  try:
304
- hdr["RAOFF"] = (self._offset_ra, "RA offset [deg]")
305
- hdr["DECOFF"] = (self._offset_dec, "Dec offset [deg]")
309
+ hdr["RAOFF"] = FitsHeaderEntry(self._offset_ra, "RA offset [deg]")
310
+ hdr["DECOFF"] = FitsHeaderEntry(self._offset_dec, "Dec offset [deg]")
306
311
  return self._filter_fits_namespace(hdr, namespaces=namespaces, **kwargs)
307
312
  except ConnectionError:
308
313
  return {}
@@ -1,21 +1,23 @@
1
1
  [project]
2
2
  name = "pyobs-alpaca"
3
- version = "2.0.0.dev2"
3
+ version = "2.0.0.dev4"
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.dev48,<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,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)