arcsecond 3.11.0__tar.gz → 3.13.0__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 (125) hide show
  1. {arcsecond-3.11.0 → arcsecond-3.13.0}/.github/workflows/pythonpublish.yml +5 -7
  2. {arcsecond-3.11.0 → arcsecond-3.13.0}/.github/workflows/tests.yml +15 -23
  3. {arcsecond-3.11.0 → arcsecond-3.13.0}/PKG-INFO +11 -11
  4. {arcsecond-3.11.0 → arcsecond-3.13.0}/README.md +10 -11
  5. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/__init__.py +0 -4
  6. arcsecond-3.13.0/arcsecond/alpaca/commands.py +180 -0
  7. arcsecond-3.13.0/arcsecond/alpaca/dome_probe.py +431 -0
  8. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/api/main.py +0 -4
  9. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cli.py +4 -4
  10. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/__init__.py +1 -3
  11. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/resources.py +0 -35
  12. arcsecond-3.13.0/arcsecond/cloud/uploader/__init__.py +8 -0
  13. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/commands.py +1 -1
  14. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/.vitepress/config.js +2 -0
  15. arcsecond-3.13.0/docs/install-windows.md +215 -0
  16. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/install.md +7 -0
  17. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/resources.md +0 -1
  18. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/upload.md +2 -40
  19. {arcsecond-3.11.0 → arcsecond-3.13.0}/pyproject.toml +12 -1
  20. arcsecond-3.13.0/tests/test_alpaca_probe.py +407 -0
  21. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/utils.py +0 -27
  22. arcsecond-3.13.0/uv.lock +2488 -0
  23. arcsecond-3.11.0/arcsecond/cloud/uploader/__init__.py +0 -13
  24. arcsecond-3.11.0/arcsecond/cloud/uploader/allskycameraimages/context.py +0 -62
  25. arcsecond-3.11.0/arcsecond/cloud/uploader/allskycameraimages/errors.py +0 -19
  26. arcsecond-3.11.0/arcsecond/cloud/uploader/allskycameraimages/uploader.py +0 -29
  27. arcsecond-3.11.0/arcsecond/cloud/uploader/allskycameraimages/utils.py +0 -54
  28. arcsecond-3.11.0/examples/example_upload_images.py +0 -24
  29. arcsecond-3.11.0/poetry.lock +0 -471
  30. arcsecond-3.11.0/tests/cloud/uploader/allskycameraimages/test_context.py +0 -115
  31. arcsecond-3.11.0/tests/cloud/uploader/allskycameraimages/test_uploader_full_process.py +0 -85
  32. arcsecond-3.11.0/tests/cloud/uploader/datafiles/__init__.py +0 -0
  33. {arcsecond-3.11.0 → arcsecond-3.13.0}/.docker/Dockerfile_postgres +0 -0
  34. {arcsecond-3.11.0 → arcsecond-3.13.0}/.docker/Dockerfile_redis +0 -0
  35. {arcsecond-3.11.0 → arcsecond-3.13.0}/.github/dependabot.yml +0 -0
  36. {arcsecond-3.11.0 → arcsecond-3.13.0}/.github/workflows/docsdeploy.yml +0 -0
  37. {arcsecond-3.11.0 → arcsecond-3.13.0}/.gitignore +0 -0
  38. {arcsecond-3.11.0 → arcsecond-3.13.0}/LICENSE +0 -0
  39. {arcsecond-3.11.0 → arcsecond-3.13.0}/Makefile +0 -0
  40. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/__version__.py +0 -0
  41. {arcsecond-3.11.0/arcsecond/cloud/uploader/allskycameraimages → arcsecond-3.13.0/arcsecond/alpaca}/__init__.py +0 -0
  42. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/api/__init__.py +0 -0
  43. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/api/config.py +0 -0
  44. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/api/constants.py +0 -0
  45. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/api/endpoint.py +0 -0
  46. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/api/resources.py +0 -0
  47. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/auth.py +0 -0
  48. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/constants.py +0 -0
  49. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/context.py +0 -0
  50. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
  51. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
  52. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
  53. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
  54. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
  55. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/errors.py +0 -0
  56. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/logger.py +0 -0
  57. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/uploader.py +0 -0
  58. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/utils.py +0 -0
  59. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploader/walker.py +0 -0
  60. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/cloud/uploads.py +0 -0
  61. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/errors.py +0 -0
  62. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/__init__.py +0 -0
  63. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/backups.py +0 -0
  64. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/checks.py +0 -0
  65. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/constants.py +0 -0
  66. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/docker/__init__.py +0 -0
  67. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/docker/constants.py +0 -0
  68. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/docker/containers.py +0 -0
  69. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/docker/docker-compose.yml +0 -0
  70. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/docker/images.py +0 -0
  71. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/docker/utils.py +0 -0
  72. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/keygen/__init__.py +0 -0
  73. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/keygen/client.py +0 -0
  74. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/keygen/utils.py +0 -0
  75. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/local.py +0 -0
  76. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/main.py +0 -0
  77. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/postgres/init-db.sh +0 -0
  78. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/setup.py +0 -0
  79. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/utils.py +0 -0
  80. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/hosting/validation.py +0 -0
  81. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/__init__.py +0 -0
  82. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/proxy.py +0 -0
  83. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/registry.py +0 -0
  84. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/sources/__init__.py +0 -0
  85. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/sources/base.py +0 -0
  86. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/sources/filewatch.py +0 -0
  87. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/imagesources/sources/opencv.py +0 -0
  88. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/options.py +0 -0
  89. {arcsecond-3.11.0 → arcsecond-3.13.0}/arcsecond/targets.py +0 -0
  90. {arcsecond-3.11.0 → arcsecond-3.13.0}/deploy.sh +0 -0
  91. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/.vitepress/theme/custom.css +0 -0
  92. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/.vitepress/theme/index.js +0 -0
  93. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/api-basics.md +0 -0
  94. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/backups.md +0 -0
  95. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/img/icon-logo.png +0 -0
  96. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/index.md +0 -0
  97. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/public/img/icon-logo.png +0 -0
  98. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/rotate-postgres-password.md +0 -0
  99. {arcsecond-3.11.0 → arcsecond-3.13.0}/docs/webcam.md +0 -0
  100. {arcsecond-3.11.0 → arcsecond-3.13.0}/examples/example_upload_files.py +0 -0
  101. {arcsecond-3.11.0 → arcsecond-3.13.0}/package-lock.json +0 -0
  102. {arcsecond-3.11.0 → arcsecond-3.13.0}/package.json +0 -0
  103. {arcsecond-3.11.0 → arcsecond-3.13.0}/requirements.txt +0 -0
  104. {arcsecond-3.11.0 → arcsecond-3.13.0}/setup.cfg +0 -0
  105. {arcsecond-3.11.0 → arcsecond-3.13.0}/setup.py +0 -0
  106. {arcsecond-3.11.0 → arcsecond-3.13.0}/sonar-project.properties +0 -0
  107. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/__init__.py +0 -0
  108. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/api/__init__.py +0 -0
  109. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/api/test_api.py +0 -0
  110. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/api/test_api_endpoint.py +0 -0
  111. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/api/test_config.py +0 -0
  112. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/api/test_targets.py +0 -0
  113. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/__init__.py +0 -0
  114. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/uploader/__init__.py +0 -0
  115. {arcsecond-3.11.0/tests/cloud/uploader/allskycameraimages → arcsecond-3.13.0/tests/cloud/uploader/datafiles}/__init__.py +0 -0
  116. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
  117. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
  118. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
  119. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
  120. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
  121. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/conftest.py +0 -0
  122. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/fixtures/file1.fits +0 -0
  123. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/test_cli.py +0 -0
  124. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/test_hosting_local.py +0 -0
  125. {arcsecond-3.11.0 → arcsecond-3.13.0}/tests/test_targets_planning.py +0 -0
@@ -25,14 +25,15 @@ jobs:
25
25
 
26
26
  - uses: actions/checkout@v5
27
27
 
28
- - name: Set up Python
29
- uses: actions/setup-python@v6
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v6
30
30
  with:
31
31
  python-version: '3.12'
32
+ enable-cache: true
32
33
 
33
34
  - name: Validate pyproject.toml version matches tag
34
35
  run: |
35
- python - <<'PY'
36
+ uv run python - <<'PY'
36
37
  import os, pathlib, tomllib
37
38
 
38
39
  tag = os.environ["GITHUB_REF_NAME"]
@@ -45,10 +46,7 @@ jobs:
45
46
  PY
46
47
 
47
48
  - name: Build distributions
48
- run: |
49
- python -m pip install --upgrade pip
50
- pip install build
51
- python -m build
49
+ run: uv build
52
50
 
53
51
  - name: Publish to PyPI (OIDC)
54
52
  uses: pypa/gh-action-pypi-publish@release/v1
@@ -16,26 +16,22 @@ jobs:
16
16
  steps:
17
17
  - uses: actions/checkout@v5
18
18
 
19
- - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v6
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
21
  with:
22
22
  python-version: ${{ matrix.python-version }}
23
+ enable-cache: true
23
24
 
24
25
  - name: Install dependencies
25
- run: |
26
- python -m pip install --upgrade pip
27
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
28
- pip install pytest pytest-cov respx
29
- pip install -e .
26
+ run: uv sync --extra webcam --group dev
30
27
 
31
28
  - name: Test with coverage (only on 3.12)
32
29
  if: matrix.python-version == '3.12'
33
- run: |
34
- pytest -vvs tests/ --cov=arcsecond --cov-report=xml:coverage.xml
30
+ run: uv run pytest -vvs tests/ --cov=arcsecond --cov-report=xml:coverage.xml
35
31
 
36
32
  - name: Test without coverage
37
33
  if: matrix.python-version != '3.12'
38
- run: pytest -vvs tests/
34
+ run: uv run pytest -vvs tests/
39
35
 
40
36
  - name: Upload coverage artifact (only on 3.12)
41
37
  if: matrix.python-version == '3.12'
@@ -58,7 +54,7 @@ jobs:
58
54
  path: .
59
55
 
60
56
  - name: SonarQube Scan
61
- uses: SonarSource/sonarqube-scan-action@v5
57
+ uses: SonarSource/sonarqube-scan-action@v6
62
58
  env:
63
59
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
64
60
 
@@ -67,24 +63,20 @@ jobs:
67
63
  steps:
68
64
  - uses: actions/checkout@v5
69
65
 
70
- - name: Set up Python
71
- uses: actions/setup-python@v6
66
+ - name: Install uv
67
+ uses: astral-sh/setup-uv@v6
72
68
  with:
73
69
  python-version: '3.12'
70
+ enable-cache: true
74
71
 
75
- - name: Install dependencies
76
- run: |
77
- python -m pip install --upgrade pip
78
- pip install flake8 black isort
72
+ - name: Install dev dependencies
73
+ run: uv sync --group dev
79
74
 
80
75
  - name: Lint with flake8
81
- run: |
82
- flake8 arcsecond tests --count --select=E9,F63,F7,F82 --show-source --statistics
76
+ run: uv run flake8 arcsecond tests --count --select=E9,F63,F7,F82 --show-source --statistics
83
77
 
84
78
  - name: Check formatting with black
85
- run: |
86
- black --check arcsecond tests
79
+ run: uv run black --check arcsecond tests
87
80
 
88
81
  - name: Check imports with isort
89
- run: |
90
- isort --check-only --profile black arcsecond tests
82
+ run: uv run isort --check-only --profile black arcsecond tests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arcsecond
3
- Version: 3.11.0
3
+ Version: 3.13.0
4
4
  Summary: CLI for arcsecond.io
5
5
  Project-URL: Homepage, https://github.com/arcsecond-io/cli
6
6
  Project-URL: Issues, https://github.com/arcsecond-io/cli/issues
@@ -36,6 +36,7 @@ Classifier: Operating System :: OS Independent
36
36
  Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
38
38
  Requires-Python: >=3.9
39
+ Requires-Dist: alpyca==3.1.2
39
40
  Requires-Dist: click>=8
40
41
  Requires-Dist: configparser
41
42
  Requires-Dist: docker
@@ -61,20 +62,19 @@ public and private ones.
61
62
 
62
63
  # Development
63
64
 
64
- To start developing the arcsecond CLI, fork the project, `git clone` it,
65
- then, in the arcsecond-cli folder, do
66
- (assuming [virtualenv](https://virtualenv.pypa.io/en/stable/) is installed):
65
+ The project uses [uv](https://docs.astral.sh/uv/) for environment and
66
+ dependency management. After forking and cloning:
67
67
 
68
68
  ```bash
69
69
  $ cd ~/arcsecond-cli
70
- $ virtualenv --python=pythonX.Y env
71
- $ source env/bin/activate
72
- $ pip install -e .
73
- ```
70
+ $ uv sync --extra webcam --group dev
71
+ $ uv run arcsecond --help
72
+ $ uv run pytest
73
+ ```
74
74
 
75
- The last line ensure you can call the "locally installed" version of the
76
- code of that folder. Once done one first time, only the `source
77
- env/bin/activate` is needed when you restart a debugging session.
75
+ `uv sync` creates `.venv/` and installs the project with its `webcam` extra
76
+ and the `dev` dependency group (pytest, black, flake8, isort). Prefix
77
+ commands with `uv run` or activate the venv with `source .venv/bin/activate`.
78
78
 
79
79
 
80
80
  ## License
@@ -11,20 +11,19 @@ public and private ones.
11
11
 
12
12
  # Development
13
13
 
14
- To start developing the arcsecond CLI, fork the project, `git clone` it,
15
- then, in the arcsecond-cli folder, do
16
- (assuming [virtualenv](https://virtualenv.pypa.io/en/stable/) is installed):
14
+ The project uses [uv](https://docs.astral.sh/uv/) for environment and
15
+ dependency management. After forking and cloning:
17
16
 
18
17
  ```bash
19
18
  $ cd ~/arcsecond-cli
20
- $ virtualenv --python=pythonX.Y env
21
- $ source env/bin/activate
22
- $ pip install -e .
23
- ```
24
-
25
- The last line ensure you can call the "locally installed" version of the
26
- code of that folder. Once done one first time, only the `source
27
- env/bin/activate` is needed when you restart a debugging session.
19
+ $ uv sync --extra webcam --group dev
20
+ $ uv run arcsecond --help
21
+ $ uv run pytest
22
+ ```
23
+
24
+ `uv sync` creates `.venv/` and installs the project with its `webcam` extra
25
+ and the `dev` dependency group (pytest, black, flake8, isort). Prefix
26
+ commands with `uv run` or activate the venv with `source .venv/bin/activate`.
28
27
 
29
28
 
30
29
  ## License
@@ -5,8 +5,6 @@ from .api import (
5
5
  ArcsecondTargetListsResource,
6
6
  )
7
7
  from .cloud.uploader import (
8
- AllSkyCameraImageFileUploader,
9
- AllSkyCameraImageUploadContext,
10
8
  DatasetFileUploader,
11
9
  DatasetUploadContext,
12
10
  )
@@ -25,8 +23,6 @@ __all__ = [
25
23
  "ArcsecondTargetPayloadPlan",
26
24
  "DatasetUploadContext",
27
25
  "DatasetFileUploader",
28
- "AllSkyCameraImageFileUploader",
29
- "AllSkyCameraImageUploadContext",
30
26
  "plan_target_payload",
31
27
  "walk_folder_and_upload_files",
32
28
  ]
@@ -0,0 +1,180 @@
1
+ """
2
+ Click command group: ``arcsecond alpaca``.
3
+
4
+ First citizen: ``arcsecond alpaca probe dome`` — a read-only diagnostic that
5
+ captures what surface a given Alpaca dome server exposes (device metadata,
6
+ ``SupportedActions``, ``CommandString`` / ``CommandBool`` / ``CommandBlind``
7
+ passthrough behaviour, and optional local host hints).
8
+
9
+ Structure leaves room for ``arcsecond alpaca probe telescope`` /
10
+ ``probe camera`` / ``probe focuser`` and ``arcsecond alpaca discover`` later
11
+ under the same group.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ import click
22
+
23
+ from ..errors import ArcsecondError
24
+ from .dome_probe import ProbeProgress, probe_dome
25
+
26
+
27
+ @click.group(name="alpaca", help="Diagnostics for local ASCOM Alpaca devices.")
28
+ def alpaca_group() -> None:
29
+ pass
30
+
31
+
32
+ @alpaca_group.group(name="probe", help="Inspect an Alpaca device read-only.")
33
+ def probe_group() -> None:
34
+ pass
35
+
36
+
37
+ _COMMON_OPTIONS = [
38
+ click.option(
39
+ "--host",
40
+ required=True,
41
+ metavar="HOST",
42
+ help="Alpaca server hostname or IP (without scheme).",
43
+ ),
44
+ click.option(
45
+ "--port",
46
+ type=int,
47
+ required=True,
48
+ metavar="PORT",
49
+ help="Alpaca server TCP port (e.g. 11111).",
50
+ ),
51
+ click.option(
52
+ "--device-number",
53
+ type=int,
54
+ default=0,
55
+ show_default=True,
56
+ help="Alpaca device number on the server.",
57
+ ),
58
+ click.option(
59
+ "--protocol",
60
+ type=click.Choice(["http", "https"], case_sensitive=False),
61
+ default="http",
62
+ show_default=True,
63
+ help="Protocol to reach the Alpaca server with.",
64
+ ),
65
+ click.option(
66
+ "--allow-active",
67
+ is_flag=True,
68
+ default=False,
69
+ help=(
70
+ "Also send CommandBlind and active-class probes. OFF by default "
71
+ "to keep the run safe on real hardware."
72
+ ),
73
+ ),
74
+ click.option(
75
+ "--collect-host-info",
76
+ is_flag=True,
77
+ default=False,
78
+ help=(
79
+ "Additionally collect best-effort local OS hints (open ports, "
80
+ "COM ProgIDs matching TCS/Galil). Read-only."
81
+ ),
82
+ ),
83
+ click.option(
84
+ "--output",
85
+ "output_path",
86
+ type=click.Path(dir_okay=False, writable=True, resolve_path=True),
87
+ default=None,
88
+ help="Where to write the JSON report. Defaults to cwd with a UTC timestamp.",
89
+ ),
90
+ ]
91
+
92
+
93
+ def _add_options(options):
94
+ def _wrap(fn):
95
+ for opt in reversed(options):
96
+ fn = opt(fn)
97
+ return fn
98
+
99
+ return _wrap
100
+
101
+
102
+ def _default_output_path(kind: str) -> str:
103
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
104
+ return str(Path.cwd() / f"alpaca_{kind}_probe_{stamp}.json")
105
+
106
+
107
+ def _format_progress(label: str, ok: bool, detail: str | None) -> str:
108
+ tag = click.style("[OK ]", fg="green") if ok else click.style("[ERR]", fg="red")
109
+ suffix = f" — {detail}" if detail else ""
110
+ return f" {tag} {label}{suffix}"
111
+
112
+
113
+ @probe_group.command(
114
+ name="dome",
115
+ help=(
116
+ "Probe a local Alpaca dome device read-only and write a JSON report.\n\n"
117
+ "Captures device metadata, SupportedActions, and the behaviour of the "
118
+ "legacy CommandString / CommandBool / CommandBlind passthroughs. "
119
+ "Useful for figuring out whether a proprietary driver (e.g. TCSGalil) "
120
+ "exposes any vendor-specific extension surface beyond the standard "
121
+ "ASCOM IDome interface."
122
+ ),
123
+ )
124
+ @_add_options(_COMMON_OPTIONS)
125
+ def probe_dome_cmd(
126
+ host: str,
127
+ port: int,
128
+ device_number: int,
129
+ protocol: str,
130
+ allow_active: bool,
131
+ collect_host_info: bool,
132
+ output_path: str | None,
133
+ ) -> None:
134
+ output_path = output_path or _default_output_path("dome")
135
+
136
+ click.echo(
137
+ click.style("Alpaca dome probe", bold=True)
138
+ + f" → {protocol}://{host}:{port} (device {device_number})"
139
+ )
140
+ if allow_active:
141
+ click.echo(
142
+ click.style(" ⚠ ", fg="yellow")
143
+ + "--allow-active is ON: CommandBlind and active-class probes will be sent."
144
+ )
145
+
146
+ progress = ProbeProgress(
147
+ on_probe=lambda label, ok, detail: click.echo(
148
+ _format_progress(label, ok, detail)
149
+ )
150
+ )
151
+
152
+ try:
153
+ result = probe_dome(
154
+ host=host,
155
+ port=port,
156
+ device_number=device_number,
157
+ protocol=protocol.lower(),
158
+ allow_active=allow_active,
159
+ collect_host_info=collect_host_info,
160
+ progress=progress,
161
+ )
162
+ except RuntimeError as exc:
163
+ raise ArcsecondError(str(exc)) from exc
164
+
165
+ try:
166
+ with open(output_path, "w", encoding="utf-8") as fp:
167
+ json.dump(result.report, fp, indent=2, sort_keys=False)
168
+ fp.write("\n")
169
+ except OSError as exc:
170
+ raise ArcsecondError(f"Could not write report to {output_path}: {exc}") from exc
171
+
172
+ counts = result.counts
173
+ supported = result.report.get("supported_actions", {})
174
+ supported_n = len(supported.get("value") or []) if supported.get("ok") else 0
175
+
176
+ click.echo("")
177
+ click.echo(click.style("Summary", bold=True))
178
+ click.echo(f" Probes: {counts.ok}/{counts.total} OK")
179
+ click.echo(f" SupportedActions: {supported_n} entries")
180
+ click.echo(f" Report written to {click.style(os.fspath(output_path), fg='cyan')}")