arcsecond 3.20.2__tar.gz → 4.1.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 (134) hide show
  1. arcsecond-4.1.0/.gitattributes +6 -0
  2. {arcsecond-3.20.2 → arcsecond-4.1.0}/.github/workflows/tests.yml +22 -0
  3. {arcsecond-3.20.2 → arcsecond-4.1.0}/PKG-INFO +35 -6
  4. {arcsecond-3.20.2 → arcsecond-4.1.0}/README.md +34 -1
  5. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/alpaca/dome_probe.py +2 -1
  6. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/alpaca/telescope_probe.py +3 -1
  7. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/api/config.py +125 -5
  8. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cli.py +35 -15
  9. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/__init__.py +3 -5
  10. arcsecond-4.1.0/arcsecond/cloud/auth.py +176 -0
  11. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploads.py +13 -2
  12. arcsecond-4.1.0/arcsecond/docgen.py +295 -0
  13. arcsecond-4.1.0/arcsecond/errors.py +18 -0
  14. arcsecond-4.1.0/arcsecond/hosting/__init__.py +22 -0
  15. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/hosting/backups.py +34 -26
  16. arcsecond-4.1.0/arcsecond/hosting/check.py +764 -0
  17. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/hosting/database.py +9 -31
  18. arcsecond-4.1.0/arcsecond/hosting/docker/__init__.py +1 -0
  19. arcsecond-4.1.0/arcsecond/hosting/lifecycle.py +328 -0
  20. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/hosting/local.py +113 -19
  21. arcsecond-4.1.0/arcsecond/hosting/stack.py +401 -0
  22. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/hosting/utils.py +40 -0
  23. arcsecond-4.1.0/arcsecond/imagesources/autostart.py +270 -0
  24. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/commands.py +57 -21
  25. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/options.py +6 -16
  26. {arcsecond-3.20.2 → arcsecond-4.1.0}/pyproject.toml +1 -5
  27. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/api/test_api_endpoint.py +21 -6
  28. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/api/test_config.py +7 -1
  29. arcsecond-4.1.0/tests/api/test_config_pointer.py +119 -0
  30. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_alpaca_probe.py +7 -8
  31. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_cli.py +2 -1
  32. arcsecond-4.1.0/tests/test_cli_api_commands.py +147 -0
  33. arcsecond-4.1.0/tests/test_docgen.py +128 -0
  34. arcsecond-4.1.0/tests/test_hosting_backups_install_dir.py +89 -0
  35. arcsecond-4.1.0/tests/test_hosting_check.py +447 -0
  36. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_hosting_database.py +1 -1
  37. arcsecond-4.1.0/tests/test_hosting_lifecycle.py +327 -0
  38. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_hosting_local.py +84 -0
  39. arcsecond-4.1.0/tests/test_hosting_stack.py +289 -0
  40. arcsecond-4.1.0/tests/test_imagesources_autostart.py +112 -0
  41. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_commands.py +93 -8
  42. {arcsecond-3.20.2 → arcsecond-4.1.0}/uv.lock +1 -84
  43. arcsecond-3.20.2/arcsecond/cloud/auth.py +0 -120
  44. arcsecond-3.20.2/arcsecond/errors.py +0 -7
  45. arcsecond-3.20.2/arcsecond/hosting/__init__.py +0 -7
  46. arcsecond-3.20.2/arcsecond/hosting/checks.py +0 -94
  47. arcsecond-3.20.2/arcsecond/hosting/constants.py +0 -8
  48. arcsecond-3.20.2/arcsecond/hosting/docker/__init__.py +0 -33
  49. arcsecond-3.20.2/arcsecond/hosting/docker/constants.py +0 -12
  50. arcsecond-3.20.2/arcsecond/hosting/docker/containers.py +0 -216
  51. arcsecond-3.20.2/arcsecond/hosting/docker/images.py +0 -43
  52. arcsecond-3.20.2/arcsecond/hosting/docker/utils.py +0 -69
  53. arcsecond-3.20.2/arcsecond/hosting/keygen/__init__.py +0 -3
  54. arcsecond-3.20.2/arcsecond/hosting/keygen/client.py +0 -233
  55. arcsecond-3.20.2/arcsecond/hosting/keygen/utils.py +0 -7
  56. arcsecond-3.20.2/arcsecond/hosting/main.py +0 -74
  57. arcsecond-3.20.2/arcsecond/hosting/postgres/init-db.sh +0 -8
  58. arcsecond-3.20.2/arcsecond/hosting/setup.py +0 -75
  59. arcsecond-3.20.2/arcsecond/hosting/validation.py +0 -47
  60. {arcsecond-3.20.2 → arcsecond-4.1.0}/.docker/Dockerfile_postgres +0 -0
  61. {arcsecond-3.20.2 → arcsecond-4.1.0}/.docker/Dockerfile_redis +0 -0
  62. {arcsecond-3.20.2 → arcsecond-4.1.0}/.flake8 +0 -0
  63. {arcsecond-3.20.2 → arcsecond-4.1.0}/.github/dependabot.yml +0 -0
  64. {arcsecond-3.20.2 → arcsecond-4.1.0}/.github/workflows/pythonpublish.yml +0 -0
  65. {arcsecond-3.20.2 → arcsecond-4.1.0}/.gitignore +0 -0
  66. {arcsecond-3.20.2 → arcsecond-4.1.0}/LICENSE +0 -0
  67. {arcsecond-3.20.2 → arcsecond-4.1.0}/Makefile +0 -0
  68. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/__init__.py +0 -0
  69. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/__version__.py +0 -0
  70. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/alpaca/__init__.py +0 -0
  71. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/alpaca/commands.py +0 -0
  72. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/alpaca/probe.py +0 -0
  73. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/api/__init__.py +0 -0
  74. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/api/constants.py +0 -0
  75. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/api/endpoint.py +0 -0
  76. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/api/main.py +0 -0
  77. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/api/resources.py +0 -0
  78. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/resources.py +0 -0
  79. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/__init__.py +0 -0
  80. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/constants.py +0 -0
  81. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/context.py +0 -0
  82. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
  83. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
  84. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
  85. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
  86. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
  87. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/errors.py +0 -0
  88. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/logger.py +0 -0
  89. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/uploader.py +0 -0
  90. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/utils.py +0 -0
  91. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/cloud/uploader/walker.py +0 -0
  92. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/hosting/docker/docker-compose.yml +0 -0
  93. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/__init__.py +0 -0
  94. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/detection.py +0 -0
  95. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/proxy.py +0 -0
  96. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/registry.py +0 -0
  97. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/runtime.py +0 -0
  98. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/sources/__init__.py +0 -0
  99. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/sources/base.py +0 -0
  100. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/sources/filewatch.py +0 -0
  101. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/sources/mdns.py +0 -0
  102. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/sources/network.py +0 -0
  103. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/sources/opencv.py +0 -0
  104. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/imagesources/store.py +0 -0
  105. {arcsecond-3.20.2 → arcsecond-4.1.0}/arcsecond/targets.py +0 -0
  106. {arcsecond-3.20.2 → arcsecond-4.1.0}/examples/example_upload_files.py +0 -0
  107. {arcsecond-3.20.2 → arcsecond-4.1.0}/requirements.txt +0 -0
  108. {arcsecond-3.20.2 → arcsecond-4.1.0}/setup.cfg +0 -0
  109. {arcsecond-3.20.2 → arcsecond-4.1.0}/setup.py +0 -0
  110. {arcsecond-3.20.2 → arcsecond-4.1.0}/sonar-project.properties +0 -0
  111. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/__init__.py +0 -0
  112. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/api/__init__.py +0 -0
  113. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/api/test_api.py +0 -0
  114. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/api/test_targets.py +0 -0
  115. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/__init__.py +0 -0
  116. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/__init__.py +0 -0
  117. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/datafiles/__init__.py +0 -0
  118. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
  119. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
  120. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
  121. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
  122. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
  123. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/conftest.py +0 -0
  124. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/fixtures/file1.fits +0 -0
  125. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_hosting_backups_restore.py +0 -0
  126. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_hosting_utils_secret_key.py +0 -0
  127. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_detection.py +0 -0
  128. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_mdns.py +0 -0
  129. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_network.py +0 -0
  130. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_proxy.py +0 -0
  131. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_store.py +0 -0
  132. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_imagesources_webcam.py +0 -0
  133. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/test_targets_planning.py +0 -0
  134. {arcsecond-3.20.2 → arcsecond-4.1.0}/tests/utils.py +0 -0
@@ -0,0 +1,6 @@
1
+ # The packaged compose template is compared byte for byte with what an
2
+ # installation holds; a CRLF checkout on Windows would make every file look
3
+ # customised. Markdown and YAML stay LF everywhere.
4
+ *.yml text eol=lf
5
+ *.yaml text eol=lf
6
+ *.md text eol=lf
@@ -40,6 +40,28 @@ jobs:
40
40
  name: coverage
41
41
  path: coverage.xml
42
42
 
43
+ # The CLI's operators run it on Windows — the observatory PC — and until
44
+ # now nothing here ever executed it there. No Docker on this runner; this
45
+ # covers the pure-Python paths: path handling, the console, the registry
46
+ # backend of the proxy's login item, and every Windows branch in the suite.
47
+ tests-windows:
48
+ runs-on: windows-latest
49
+
50
+ steps:
51
+ - uses: actions/checkout@v5
52
+
53
+ - name: Install uv
54
+ uses: astral-sh/setup-uv@v6
55
+ with:
56
+ python-version: '3.12'
57
+ enable-cache: true
58
+
59
+ - name: Install dependencies
60
+ run: uv sync --extra webcam --group dev
61
+
62
+ - name: Test
63
+ run: uv run pytest -q tests/
64
+
43
65
  scan:
44
66
  runs-on: ubuntu-latest
45
67
  needs: tests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arcsecond
3
- Version: 3.20.2
3
+ Version: 4.1.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
@@ -40,12 +40,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
40
  Requires-Python: >=3.12
41
41
  Requires-Dist: alpyca==3.1.2
42
42
  Requires-Dist: click>=8
43
- Requires-Dist: configparser
44
- Requires-Dist: docker
45
43
  Requires-Dist: httpx
46
- Requires-Dist: py-machineid
47
44
  Requires-Dist: tqdm<5.0.0,>=4.67.1
48
- Requires-Dist: wait-for-it
49
45
  Provides-Extra: webcam
50
46
  Requires-Dist: aiohttp>=3.9; extra == 'webcam'
51
47
  Requires-Dist: opencv-python-headless<5,>=4.10; extra == 'webcam'
@@ -60,7 +56,40 @@ The Command-line interface (CLI) for Arcsecond. It can be used as Python
60
56
  module too. The CLI makes it easy to login/register and access Arcsecond resources,
61
57
  public and private ones.
62
58
 
63
- [Read the docs](https://docs.arcsecond.io/cli)
59
+ [Read the docs](https://docs.arcsecond.io/reference/cli)
60
+
61
+ ## Running Arcsecond.local
62
+
63
+ The CLI owns the whole lifecycle of a self-hosted installation; there is no
64
+ `docker` command to type:
65
+
66
+ ```bash
67
+ $ arcsecond setup --lan-host 192.168.1.42 # writes .env and docker-compose.yml
68
+ $ arcsecond start # first run downloads the images
69
+ $ arcsecond status
70
+ $ arcsecond logs backend -f
71
+ $ arcsecond restart backend worker # after editing .env
72
+ $ arcsecond update # newer images, refreshed compose file
73
+ $ arcsecond stop
74
+ ```
75
+
76
+ Every one of these finds the installation in the current folder, or in the
77
+ folder `arcsecond setup` last ran in, or where `--dir` points.
78
+
79
+ ## Which server the CLI talks to
80
+
81
+ The CLI points at one API server at a time — the cloud by default, or a
82
+ server you registered, typically your own Arcsecond.local — and every command
83
+ follows that pointer:
84
+
85
+ ```bash
86
+ $ arcsecond api # list them, * marks the current one
87
+ $ arcsecond api use local # `setup` registered `local` for you
88
+ $ arcsecond login # on that server
89
+ ```
90
+
91
+ Credentials are kept per server. For a script or a cron job, `ARCSECOND_API=<name>`
92
+ selects a server for that process alone, without moving the pointer.
64
93
 
65
94
  ## Configuration
66
95
 
@@ -7,7 +7,40 @@ The Command-line interface (CLI) for Arcsecond. It can be used as Python
7
7
  module too. The CLI makes it easy to login/register and access Arcsecond resources,
8
8
  public and private ones.
9
9
 
10
- [Read the docs](https://docs.arcsecond.io/cli)
10
+ [Read the docs](https://docs.arcsecond.io/reference/cli)
11
+
12
+ ## Running Arcsecond.local
13
+
14
+ The CLI owns the whole lifecycle of a self-hosted installation; there is no
15
+ `docker` command to type:
16
+
17
+ ```bash
18
+ $ arcsecond setup --lan-host 192.168.1.42 # writes .env and docker-compose.yml
19
+ $ arcsecond start # first run downloads the images
20
+ $ arcsecond status
21
+ $ arcsecond logs backend -f
22
+ $ arcsecond restart backend worker # after editing .env
23
+ $ arcsecond update # newer images, refreshed compose file
24
+ $ arcsecond stop
25
+ ```
26
+
27
+ Every one of these finds the installation in the current folder, or in the
28
+ folder `arcsecond setup` last ran in, or where `--dir` points.
29
+
30
+ ## Which server the CLI talks to
31
+
32
+ The CLI points at one API server at a time — the cloud by default, or a
33
+ server you registered, typically your own Arcsecond.local — and every command
34
+ follows that pointer:
35
+
36
+ ```bash
37
+ $ arcsecond api # list them, * marks the current one
38
+ $ arcsecond api use local # `setup` registered `local` for you
39
+ $ arcsecond login # on that server
40
+ ```
41
+
42
+ Credentials are kept per server. For a script or a cron job, `ARCSECOND_API=<name>`
43
+ selects a server for that process alone, without moving the pointer.
11
44
 
12
45
  ## Configuration
13
46
 
@@ -17,8 +17,9 @@ from __future__ import annotations
17
17
 
18
18
  from typing import Any, Callable, Iterable
19
19
 
20
- from .probe import ProbeProgress, ProbeResult, probe_device
20
+ from .probe import ProbeProgress, ProbeResult
21
21
  from .probe import iter_probe_labels as _iter_probe_labels
22
+ from .probe import probe_device
22
23
 
23
24
  # Read-only device descriptors and dome state, in the order we want them
24
25
  # presented in the report. Names are PascalCase to match the alpyca surface.
@@ -27,10 +27,12 @@ from .probe import (
27
27
  ProbeProgress,
28
28
  ProbeResult,
29
29
  capture,
30
+ )
31
+ from .probe import iter_probe_labels as _iter_probe_labels
32
+ from .probe import (
30
33
  probe_device,
31
34
  record,
32
35
  )
33
- from .probe import iter_probe_labels as _iter_probe_labels
34
36
 
35
37
  # Read-only descriptors, optics, site, pointing, motion state and capabilities,
36
38
  # in the order we want them presented. Names are PascalCase to match alpyca.
@@ -22,10 +22,31 @@ from .constants import ARCSECOND_API_URL_PROD
22
22
  # to end up in the same place as the command that started them.
23
23
  CONFIG_DIR_ENV_VAR = "ARCSECOND_CONFIG_DIR"
24
24
 
25
+ # Which API server the CLI talks to, when a command does not say.
26
+ #
27
+ # Every registered server has a section of its own in config.ini — its
28
+ # address and the credentials for it together — and this is the name of the
29
+ # one currently pointed at. It is set with `arcsecond api use <name>` and
30
+ # stays set: every later command uses it, so nobody has to type a server on
31
+ # each one. The environment variable is the one-shot override for scripts and
32
+ # cron jobs, which should not move the pointer of whoever sits at the keyboard.
33
+ API_NAME_ENV_VAR = "ARCSECOND_API"
34
+ DEFAULT_API_NAME = "cloud"
35
+
36
+ # The section that holds the CLI's own settings (the pointer above, the
37
+ # remembered Arcsecond.local install directory...). It is not an API server and
38
+ # must never be read as one. Not configparser's DEFAULT section: values placed
39
+ # there leak into every other section.
40
+ CLI_SECTION = "cli"
41
+ CLI_KEY_API = "api"
42
+
25
43
 
26
44
  class ArcsecondConfig(object):
27
45
  def __init__(self, **kwargs):
28
- self.__api_name = kwargs.get("api_name", "cloud") or "cloud"
46
+ api_name = kwargs.get("api_name") or ArcsecondConfig.current_api_name()
47
+ if api_name == CLI_SECTION:
48
+ raise ArcsecondError(f"'{CLI_SECTION}' is reserved and cannot name an API.")
49
+ self.__api_name = api_name
29
50
  self.__verbose = kwargs.get("verbose", 0) or 0
30
51
  if "config" in kwargs.keys():
31
52
  self.__config = kwargs.get("config")
@@ -40,6 +61,97 @@ class ArcsecondConfig(object):
40
61
  def from_state(cls, state: State):
41
62
  return cls(api_name=state.api_name, verbose=state.verbose)
42
63
 
64
+ # ------------------------------------------------------------------
65
+ # The CLI's own settings: the API pointer and friends
66
+ # ------------------------------------------------------------------
67
+
68
+ @classmethod
69
+ def _read_parser(cls) -> ConfigParser:
70
+ parser = ConfigParser()
71
+ parser.read(str(cls.file_path()))
72
+ return parser
73
+
74
+ @classmethod
75
+ def _write_parser(cls, parser: ConfigParser) -> None:
76
+ with open(cls.file_path(), "w") as f:
77
+ parser.write(f)
78
+
79
+ @classmethod
80
+ def read_cli_setting(cls, key: str, default: Optional[str] = None) -> Optional[str]:
81
+ parser = cls._read_parser()
82
+ if not parser.has_section(CLI_SECTION):
83
+ return default
84
+ value = parser[CLI_SECTION].get(key, "").strip()
85
+ return value or default
86
+
87
+ @classmethod
88
+ def write_cli_setting(cls, key: str, value: Optional[str]) -> None:
89
+ """Set one CLI setting; None removes it."""
90
+ parser = cls._read_parser()
91
+ if not parser.has_section(CLI_SECTION):
92
+ parser.add_section(CLI_SECTION)
93
+ if value is None:
94
+ parser.remove_option(CLI_SECTION, key)
95
+ else:
96
+ parser[CLI_SECTION][key] = value
97
+ cls._write_parser(parser)
98
+
99
+ @classmethod
100
+ def current_api_name(cls) -> str:
101
+ """The API the CLI points at: $ARCSECOND_API, else the recorded
102
+ pointer, else 'cloud'."""
103
+ override = os.environ.get(API_NAME_ENV_VAR, "").strip()
104
+ if override:
105
+ return override
106
+ return cls.read_cli_setting(CLI_KEY_API) or DEFAULT_API_NAME
107
+
108
+ @classmethod
109
+ def is_api_name_overridden(cls) -> bool:
110
+ return bool(os.environ.get(API_NAME_ENV_VAR, "").strip())
111
+
112
+ @classmethod
113
+ def set_current_api_name(cls, name: str) -> None:
114
+ if name not in cls.registered_api_names():
115
+ raise ArcsecondError(
116
+ f"No API server is registered under the name '{name}'. "
117
+ "Register it first: arcsecond api add <name> <address>"
118
+ )
119
+ cls.write_cli_setting(CLI_KEY_API, name)
120
+
121
+ @classmethod
122
+ def registered_api_names(cls) -> list:
123
+ """'cloud' first, then every section that carries a server address."""
124
+ parser = cls._read_parser()
125
+ names = [DEFAULT_API_NAME]
126
+ for section_name in parser.sections():
127
+ if section_name in (CLI_SECTION, DEFAULT_API_NAME):
128
+ continue
129
+ if parser[section_name].get("api_server", "").strip():
130
+ names.append(section_name)
131
+ return names
132
+
133
+ @classmethod
134
+ def remove_api(cls, name: str) -> None:
135
+ """Forget a registered server, credentials included. The pointer falls
136
+ back to 'cloud' if it was aimed at the removed one."""
137
+ if name == DEFAULT_API_NAME:
138
+ raise ArcsecondError("The 'cloud' API server cannot be removed.")
139
+ parser = cls._read_parser()
140
+ if (
141
+ not parser.has_section(name)
142
+ or not parser[name].get("api_server", "").strip()
143
+ ):
144
+ raise ArcsecondError(
145
+ f"No API server is registered under the name '{name}'."
146
+ )
147
+ parser.remove_section(name)
148
+ if (
149
+ parser.has_section(CLI_SECTION)
150
+ and parser[CLI_SECTION].get(CLI_KEY_API) == name
151
+ ):
152
+ parser.remove_option(CLI_SECTION, CLI_KEY_API)
153
+ cls._write_parser(parser)
154
+
43
155
  @classmethod
44
156
  def __old_config_file_path(cls):
45
157
  return (Path.home() / ".arcsecond.ini").expanduser()
@@ -118,12 +230,20 @@ class ArcsecondConfig(object):
118
230
 
119
231
  @property
120
232
  def all_apis(self) -> Optional[str]:
121
- apis = [f"cloud: {ARCSECOND_API_URL_PROD} (protected)"]
233
+ """One line per registered server, the current one marked with '*'."""
234
+ current = ArcsecondConfig.current_api_name()
235
+
236
+ def line(name, address, note=""):
237
+ mark = "*" if name == current else " "
238
+ return f" {mark} {name}: {address}{note}"
239
+
240
+ apis = [line(DEFAULT_API_NAME, ARCSECOND_API_URL_PROD, " (protected)")]
122
241
  for section_name in self.__config.sections():
123
- section = self.__config[section_name]
124
- api_server = section.get("api_server", "")
242
+ if section_name in (CLI_SECTION, DEFAULT_API_NAME):
243
+ continue
244
+ api_server = self.__config[section_name].get("api_server", "")
125
245
  if api_server:
126
- apis.append(f"{section_name}: {api_server}")
246
+ apis.append(line(section_name, api_server))
127
247
  return "\n".join(apis)
128
248
 
129
249
  @property
@@ -5,11 +5,23 @@ from arcsecond.cloud import (
5
5
  api,
6
6
  datasets,
7
7
  login,
8
- me,
9
8
  telescopes,
9
+ upload,
10
10
  upload_data,
11
11
  )
12
- from arcsecond.hosting import backups, db, setup
12
+ from arcsecond.docgen import docs
13
+ from arcsecond.hosting import (
14
+ backups,
15
+ check_cmd,
16
+ db,
17
+ logs,
18
+ restart,
19
+ setup,
20
+ start,
21
+ status,
22
+ stop,
23
+ update,
24
+ )
13
25
  from arcsecond.imagesources import commands as imagesources
14
26
 
15
27
  from . import __version__
@@ -37,26 +49,32 @@ def version():
37
49
  click.echo(__version__.__version__)
38
50
 
39
51
 
40
- # Read/set API server to use.
52
+ # Which API server every command talks to (`arcsecond api use <name>`).
41
53
  main.add_command(api)
42
54
 
43
- # Login to Arcsecond.
55
+ # Login to that server.
44
56
  main.add_command(login)
45
57
 
46
- # Read the logged-in user profile.
47
- main.add_command(me)
48
-
49
- # Read the list of existing datasets (for upload purposes).
58
+ # Read the list of existing datasets / telescopes (for upload purposes).
50
59
  main.add_command(datasets)
51
-
52
- # Read the list of existing telescopes (for upload purposes).
53
60
  main.add_command(telescopes)
54
61
 
55
- # Upload a folder of files to a given dataset.
62
+ # Upload a folder of files to a given dataset. `upload-data` is the pre-4.0
63
+ # name, kept hidden so existing scripts keep working.
64
+ main.add_command(upload)
56
65
  main.add_command(upload_data)
57
66
 
58
- # Allow to try arcsecond by installing a local version
67
+ # Arcsecond.local: write the installation, then run it — no `docker` to type.
59
68
  main.add_command(setup)
69
+ main.add_command(start)
70
+ main.add_command(stop)
71
+ main.add_command(restart)
72
+ main.add_command(status)
73
+ main.add_command(logs)
74
+ main.add_command(update)
75
+
76
+ # Is it reachable from the rest of the observatory, and if not, why not.
77
+ main.add_command(check_cmd)
60
78
 
61
79
  # Browse and restore Arcsecond.local DB backups.
62
80
  main.add_command(backups)
@@ -73,12 +91,14 @@ main.add_command(imagesources.webcam)
73
91
  main.add_command(imagesources.allsky)
74
92
  main.add_command(imagesources.proxy)
75
93
 
76
- # Removed: a network camera is a webcam. Kept only to say so — see commands.py.
77
- main.add_command(imagesources.netcam)
78
-
79
94
  # Local ASCOM Alpaca diagnostics (e.g. `arcsecond alpaca probe dome ...`).
80
95
  main.add_command(alpaca_group)
81
96
 
97
+ # The tool describing itself: `arcsecond docs commands --out DIR` writes the
98
+ # command reference the documentation site publishes. Hidden — it is for the
99
+ # documentation build, not for operators.
100
+ main.add_command(docs)
101
+
82
102
 
83
103
  # `arcsecond proxy start` launches its detached proxy as
84
104
  # `python -m arcsecond.cli proxy start --foreground`, which needs this. Going
@@ -1,24 +1,22 @@
1
- from .auth import api, login, me
1
+ from .auth import api, login
2
2
  from .resources import datasets, telescopes
3
3
  from .uploader import (
4
4
  DatasetFileUploader,
5
5
  DatasetUploadContext,
6
6
  )
7
7
  from .uploader.constants import Status, Substatus
8
- from .uploads import (
9
- upload_data,
10
- )
8
+ from .uploads import upload, upload_data
11
9
 
12
10
  # Re-exported: this is the package's public surface.
13
11
  __all__ = [
14
12
  "api",
15
13
  "login",
16
- "me",
17
14
  "datasets",
18
15
  "telescopes",
19
16
  "DatasetFileUploader",
20
17
  "DatasetUploadContext",
21
18
  "Status",
22
19
  "Substatus",
20
+ "upload",
23
21
  "upload_data",
24
22
  ]
@@ -0,0 +1,176 @@
1
+ import click
2
+ import httpx
3
+
4
+ from arcsecond.api import ArcsecondAPI, ArcsecondConfig
5
+ from arcsecond.api.config import API_NAME_ENV_VAR, DEFAULT_API_NAME
6
+ from arcsecond.errors import ArcsecondError
7
+ from arcsecond.options import State, basic_options
8
+
9
+ pass_state = click.make_pass_decorator(State, ensure=True)
10
+
11
+
12
+ @click.command(short_help="Login to your Arcsecond account.")
13
+ @click.option(
14
+ "--username",
15
+ required=True,
16
+ nargs=1,
17
+ prompt=True,
18
+ help="Account username (without @). Primary email address is also allowed.",
19
+ )
20
+ @click.option(
21
+ "--type",
22
+ required=True,
23
+ type=click.Choice(["access", "upload"], case_sensitive=False),
24
+ prompt=True,
25
+ help="Your access key (a.k.a. API key). Visit your settings page to copy "
26
+ "and paste it here. One of Access or Upload key must be provided.",
27
+ )
28
+ @click.option(
29
+ "--key",
30
+ required=True,
31
+ nargs=1,
32
+ prompt=True,
33
+ help="Your upload key. Visit your settings page to copy and paste it here. One of Access or Upload key must be provided.",
34
+ )
35
+ @basic_options
36
+ @pass_state
37
+ def login(state, username, type, key):
38
+ """Login to your Arcsecond account, on the API server the CLI points at.
39
+
40
+ You must provide either your Access Key, or your Upload Key.
41
+ By doing so, you choose the level of access you want to store
42
+ on this computer. The Access Key give a full API access to your
43
+ data. The Upload Key gives just enough permissions to upload data.
44
+
45
+ Both keys can be retrieved from your personal Settings page on
46
+ https://www.arcsecond.io (or on your own Arcsecond.local).
47
+
48
+ Beware that the Key you provide will be stored locally on the file:
49
+ ~/.config/arcsecond/config.ini
50
+ """
51
+ key_name = "access_key" if type == "access" else "upload_key"
52
+ config = ArcsecondConfig.from_state(state)
53
+ _, error = ArcsecondAPI(config).login(username, **{key_name: key})
54
+ if error:
55
+ click.echo(str(error))
56
+ return
57
+ click.echo(
58
+ click.style("Logged in", fg="green")
59
+ + f" as @{username} on '{config.api_name}' ({config.api_server})."
60
+ )
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # `arcsecond api`: the pointer to the server every command talks to
65
+ # ---------------------------------------------------------------------------
66
+
67
+ PROBE_TIMEOUT = 8.0 # seconds
68
+
69
+
70
+ def probe_api_server(address: str):
71
+ """Whether something answers HTTP at ``address``. Returns (ok, detail).
72
+
73
+ A reachability check, not a fingerprint: an Arcsecond server answers its
74
+ root with 200, but the point is to refuse a typo or a server that is not
75
+ up yet, and any HTTP answer below 500 proves the address is a live one.
76
+ """
77
+ try:
78
+ response = httpx.get(address, timeout=PROBE_TIMEOUT, follow_redirects=True)
79
+ except httpx.HTTPError as e:
80
+ return False, f"{type(e).__name__}: {e}"
81
+ if response.status_code >= 500:
82
+ return False, f"the server answered {response.status_code}"
83
+ return True, f"answered {response.status_code}"
84
+
85
+
86
+ class _ApiGroup(click.Group):
87
+ """`arcsecond api <name> <address>`, the pre-4.0 way of registering a
88
+ server, still works: two tokens that are not a subcommand mean `add`."""
89
+
90
+ def resolve_command(self, ctx, args):
91
+ if args and args[0] not in self.commands and len(args) == 2:
92
+ return super().resolve_command(ctx, ["add", *args])
93
+ return super().resolve_command(ctx, args)
94
+
95
+
96
+ @click.group(cls=_ApiGroup, invoke_without_command=True)
97
+ @click.pass_context
98
+ def api(ctx):
99
+ """The API server the CLI talks to.
100
+
101
+ Every command uses one server: the "cloud" (api.arcsecond.io) by
102
+ default, or any server registered here — typically your own
103
+ Arcsecond.local. Point the CLI at one and it stays pointed there:
104
+
105
+ \b
106
+ arcsecond api list the servers, * marks the current one
107
+ arcsecond api add local http://localhost:8800
108
+ arcsecond api use local every command now talks to it
109
+ arcsecond api remove local
110
+
111
+ Credentials are kept per server, so `arcsecond login` after `api use`
112
+ logs you in on that server only.
113
+
114
+ For a script or a cron job, set ARCSECOND_API=<name> instead: it applies to
115
+ that process alone and leaves the pointer untouched.
116
+ """
117
+ if ctx.invoked_subcommand is None:
118
+ config = ArcsecondConfig()
119
+ click.echo(" • Registered API servers (* = current):")
120
+ click.echo(config.all_apis)
121
+ if ArcsecondConfig.is_api_name_overridden():
122
+ click.echo(
123
+ f"\n {API_NAME_ENV_VAR} is set, which overrides the recorded pointer "
124
+ "for this process."
125
+ )
126
+
127
+
128
+ @api.command(name="add", help="Register a server under a name.")
129
+ @click.argument("name", nargs=1)
130
+ @click.argument("address", nargs=1)
131
+ def api_add(name, address):
132
+ if name == DEFAULT_API_NAME:
133
+ raise ArcsecondError(
134
+ "You cannot change the server address of the 'cloud' API server."
135
+ )
136
+ config = ArcsecondConfig(api_name=name)
137
+ config.api_server = address
138
+ click.echo(f' • Registered the API "{name}" at "{config.api_server}".')
139
+ if ArcsecondConfig.current_api_name() != name:
140
+ click.echo(f" Point the CLI at it with: arcsecond api use {name}")
141
+
142
+
143
+ @api.command(
144
+ name="use",
145
+ help="Point every following command at this server (it must answer).",
146
+ )
147
+ @click.argument("name", nargs=1)
148
+ def api_use(name):
149
+ if name not in ArcsecondConfig.registered_api_names():
150
+ raise ArcsecondError(
151
+ f"No API server is registered under the name '{name}'.\n"
152
+ f"Register it first: arcsecond api add {name} <address>"
153
+ )
154
+ address = ArcsecondConfig(api_name=name).api_server
155
+ ok, detail = probe_api_server(address)
156
+ if not ok:
157
+ raise ArcsecondError(
158
+ f"'{name}' ({address}) does not answer — {detail}.\n"
159
+ "The pointer was not moved. Start that server, or check the address."
160
+ )
161
+ ArcsecondConfig.set_current_api_name(name)
162
+ click.echo(
163
+ click.style(" • The CLI now points at ", fg="green") + f"'{name}' ({address})."
164
+ )
165
+ if not ArcsecondConfig(api_name=name).is_logged_in:
166
+ click.echo(" You are not logged in there yet: arcsecond login")
167
+
168
+
169
+ @api.command(name="remove", help="Forget a registered server and its credentials.")
170
+ @click.argument("name", nargs=1)
171
+ def api_remove(name):
172
+ was_current = ArcsecondConfig.current_api_name() == name
173
+ ArcsecondConfig.remove_api(name)
174
+ click.echo(f' • Removed the API "{name}".')
175
+ if was_current:
176
+ click.echo(f" The CLI points at '{DEFAULT_API_NAME}' again.")
@@ -14,7 +14,7 @@ from arcsecond.options import State, basic_options
14
14
  pass_state = click.make_pass_decorator(State, ensure=True)
15
15
 
16
16
 
17
- @click.command()
17
+ @click.command(name="upload")
18
18
  @click.argument("folder", required=True, nargs=1)
19
19
  @click.option(
20
20
  "-d",
@@ -55,7 +55,7 @@ pass_state = click.make_pass_decorator(State, ensure=True)
55
55
  )
56
56
  @basic_options
57
57
  @pass_state
58
- def upload_data(
58
+ def upload(
59
59
  state, folder, dataset=None, telescope=None, raw=None, tags=None, portal=None
60
60
  ):
61
61
  """
@@ -105,3 +105,14 @@ def upload_data(
105
105
  ok = input("\n ----> OK? (Press Enter) ")
106
106
  if ok.strip() == "":
107
107
  walk_folder_and_upload_files(DatasetFileUploader, context, folder)
108
+
109
+
110
+ # The pre-4.0 name. Same command object underneath, so the two never drift;
111
+ # hidden so that `--help` shows one way of doing it.
112
+ upload_data = click.Command(
113
+ name="upload-data",
114
+ callback=upload.callback,
115
+ params=upload.params,
116
+ help=upload.help,
117
+ hidden=True,
118
+ )