arcsecond 3.20.2__tar.gz → 4.2.1__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.
- arcsecond-4.2.1/.gitattributes +6 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.github/workflows/tests.yml +22 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/PKG-INFO +36 -6
- {arcsecond-3.20.2 → arcsecond-4.2.1}/README.md +35 -1
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/alpaca/dome_probe.py +2 -1
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/alpaca/telescope_probe.py +3 -1
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/api/config.py +125 -5
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cli.py +39 -15
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/__init__.py +3 -5
- arcsecond-4.2.1/arcsecond/cloud/auth.py +176 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploads.py +13 -2
- arcsecond-4.2.1/arcsecond/docgen.py +341 -0
- arcsecond-4.2.1/arcsecond/errors.py +18 -0
- arcsecond-4.2.1/arcsecond/hosting/__init__.py +24 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/hosting/backups.py +34 -26
- arcsecond-4.2.1/arcsecond/hosting/check.py +764 -0
- arcsecond-4.2.1/arcsecond/hosting/composedoc.py +411 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/hosting/database.py +9 -31
- arcsecond-4.2.1/arcsecond/hosting/docker/__init__.py +1 -0
- arcsecond-4.2.1/arcsecond/hosting/lifecycle.py +328 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/hosting/local.py +153 -19
- arcsecond-4.2.1/arcsecond/hosting/stack.py +400 -0
- arcsecond-4.2.1/arcsecond/hosting/token.py +184 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/hosting/utils.py +40 -0
- arcsecond-4.2.1/arcsecond/imagesources/autostart.py +270 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/commands.py +57 -21
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/options.py +6 -16
- {arcsecond-3.20.2 → arcsecond-4.2.1}/pyproject.toml +1 -5
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/api/test_api_endpoint.py +21 -6
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/api/test_config.py +7 -1
- arcsecond-4.2.1/tests/api/test_config_pointer.py +119 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_alpaca_probe.py +7 -8
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_cli.py +2 -1
- arcsecond-4.2.1/tests/test_cli_api_commands.py +147 -0
- arcsecond-4.2.1/tests/test_docgen.py +141 -0
- arcsecond-4.2.1/tests/test_hosting_backups_install_dir.py +89 -0
- arcsecond-4.2.1/tests/test_hosting_check.py +447 -0
- arcsecond-4.2.1/tests/test_hosting_composedoc.py +110 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_hosting_database.py +1 -1
- arcsecond-4.2.1/tests/test_hosting_lifecycle.py +327 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_hosting_local.py +86 -0
- arcsecond-4.2.1/tests/test_hosting_stack.py +289 -0
- arcsecond-4.2.1/tests/test_hosting_token.py +163 -0
- arcsecond-4.2.1/tests/test_imagesources_autostart.py +112 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_commands.py +93 -8
- {arcsecond-3.20.2 → arcsecond-4.2.1}/uv.lock +1 -84
- arcsecond-3.20.2/arcsecond/cloud/auth.py +0 -120
- arcsecond-3.20.2/arcsecond/errors.py +0 -7
- arcsecond-3.20.2/arcsecond/hosting/__init__.py +0 -7
- arcsecond-3.20.2/arcsecond/hosting/checks.py +0 -94
- arcsecond-3.20.2/arcsecond/hosting/constants.py +0 -8
- arcsecond-3.20.2/arcsecond/hosting/docker/__init__.py +0 -33
- arcsecond-3.20.2/arcsecond/hosting/docker/constants.py +0 -12
- arcsecond-3.20.2/arcsecond/hosting/docker/containers.py +0 -216
- arcsecond-3.20.2/arcsecond/hosting/docker/images.py +0 -43
- arcsecond-3.20.2/arcsecond/hosting/docker/utils.py +0 -69
- arcsecond-3.20.2/arcsecond/hosting/keygen/__init__.py +0 -3
- arcsecond-3.20.2/arcsecond/hosting/keygen/client.py +0 -233
- arcsecond-3.20.2/arcsecond/hosting/keygen/utils.py +0 -7
- arcsecond-3.20.2/arcsecond/hosting/main.py +0 -74
- arcsecond-3.20.2/arcsecond/hosting/postgres/init-db.sh +0 -8
- arcsecond-3.20.2/arcsecond/hosting/setup.py +0 -75
- arcsecond-3.20.2/arcsecond/hosting/validation.py +0 -47
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.docker/Dockerfile_redis +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.flake8 +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.github/dependabot.yml +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.github/workflows/pythonpublish.yml +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/.gitignore +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/LICENSE +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/Makefile +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/__version__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/alpaca/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/alpaca/commands.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/alpaca/probe.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/api/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/api/constants.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/api/endpoint.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/api/main.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/api/resources.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/hosting/docker/docker-compose.yml +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/detection.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/proxy.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/registry.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/runtime.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/sources/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/sources/base.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/sources/filewatch.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/sources/mdns.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/sources/network.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/sources/opencv.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/imagesources/store.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/arcsecond/targets.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/examples/example_upload_files.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/requirements.txt +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/setup.cfg +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/setup.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/sonar-project.properties +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/api/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/api/test_api.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/api/test_targets.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/conftest.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/fixtures/file1.fits +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_hosting_backups_restore.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_hosting_utils_secret_key.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_detection.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_mdns.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_network.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_proxy.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_store.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_imagesources_webcam.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/test_targets_planning.py +0 -0
- {arcsecond-3.20.2 → arcsecond-4.2.1}/tests/utils.py +0 -0
|
@@ -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
|
+
Version: 4.2.1
|
|
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,41 @@ 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 token set # the access token Arcsecond gave you, once per machine (setup asks too)
|
|
69
|
+
$ arcsecond start # first run downloads the images
|
|
70
|
+
$ arcsecond status
|
|
71
|
+
$ arcsecond logs backend -f
|
|
72
|
+
$ arcsecond restart backend worker # after editing .env
|
|
73
|
+
$ arcsecond update # newer images, refreshed compose file
|
|
74
|
+
$ arcsecond stop
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every one of these finds the installation in the current folder, or in the
|
|
78
|
+
folder `arcsecond setup` last ran in, or where `--dir` points.
|
|
79
|
+
|
|
80
|
+
## Which server the CLI talks to
|
|
81
|
+
|
|
82
|
+
The CLI points at one API server at a time — the cloud by default, or a
|
|
83
|
+
server you registered, typically your own Arcsecond.local — and every command
|
|
84
|
+
follows that pointer:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
$ arcsecond api # list them, * marks the current one
|
|
88
|
+
$ arcsecond api use local # `setup` registered `local` for you
|
|
89
|
+
$ arcsecond login # on that server
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Credentials are kept per server. For a script or a cron job, `ARCSECOND_API=<name>`
|
|
93
|
+
selects a server for that process alone, without moving the pointer.
|
|
64
94
|
|
|
65
95
|
## Configuration
|
|
66
96
|
|
|
@@ -7,7 +7,41 @@ 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 token set # the access token Arcsecond gave you, once per machine (setup asks too)
|
|
20
|
+
$ arcsecond start # first run downloads the images
|
|
21
|
+
$ arcsecond status
|
|
22
|
+
$ arcsecond logs backend -f
|
|
23
|
+
$ arcsecond restart backend worker # after editing .env
|
|
24
|
+
$ arcsecond update # newer images, refreshed compose file
|
|
25
|
+
$ arcsecond stop
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every one of these finds the installation in the current folder, or in the
|
|
29
|
+
folder `arcsecond setup` last ran in, or where `--dir` points.
|
|
30
|
+
|
|
31
|
+
## Which server the CLI talks to
|
|
32
|
+
|
|
33
|
+
The CLI points at one API server at a time — the cloud by default, or a
|
|
34
|
+
server you registered, typically your own Arcsecond.local — and every command
|
|
35
|
+
follows that pointer:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
$ arcsecond api # list them, * marks the current one
|
|
39
|
+
$ arcsecond api use local # `setup` registered `local` for you
|
|
40
|
+
$ arcsecond login # on that server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Credentials are kept per server. For a script or a cron job, `ARCSECOND_API=<name>`
|
|
44
|
+
selects a server for that process alone, without moving the pointer.
|
|
11
45
|
|
|
12
46
|
## Configuration
|
|
13
47
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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(
|
|
246
|
+
apis.append(line(section_name, api_server))
|
|
127
247
|
return "\n".join(apis)
|
|
128
248
|
|
|
129
249
|
@property
|
|
@@ -5,11 +5,24 @@ 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.
|
|
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
|
+
token_group,
|
|
24
|
+
update,
|
|
25
|
+
)
|
|
13
26
|
from arcsecond.imagesources import commands as imagesources
|
|
14
27
|
|
|
15
28
|
from . import __version__
|
|
@@ -37,26 +50,35 @@ def version():
|
|
|
37
50
|
click.echo(__version__.__version__)
|
|
38
51
|
|
|
39
52
|
|
|
40
|
-
#
|
|
53
|
+
# Which API server every command talks to (`arcsecond api use <name>`).
|
|
41
54
|
main.add_command(api)
|
|
42
55
|
|
|
43
|
-
# Login to
|
|
56
|
+
# Login to that server.
|
|
44
57
|
main.add_command(login)
|
|
45
58
|
|
|
46
|
-
# Read the
|
|
47
|
-
main.add_command(me)
|
|
48
|
-
|
|
49
|
-
# Read the list of existing datasets (for upload purposes).
|
|
59
|
+
# Read the list of existing datasets / telescopes (for upload purposes).
|
|
50
60
|
main.add_command(datasets)
|
|
51
|
-
|
|
52
|
-
# Read the list of existing telescopes (for upload purposes).
|
|
53
61
|
main.add_command(telescopes)
|
|
54
62
|
|
|
55
|
-
# Upload a folder of files to a given dataset.
|
|
63
|
+
# Upload a folder of files to a given dataset. `upload-data` is the pre-4.0
|
|
64
|
+
# name, kept hidden so existing scripts keep working.
|
|
65
|
+
main.add_command(upload)
|
|
56
66
|
main.add_command(upload_data)
|
|
57
67
|
|
|
58
|
-
#
|
|
68
|
+
# Arcsecond.local: write the installation, then run it — no `docker` to type.
|
|
59
69
|
main.add_command(setup)
|
|
70
|
+
main.add_command(start)
|
|
71
|
+
main.add_command(stop)
|
|
72
|
+
main.add_command(restart)
|
|
73
|
+
main.add_command(status)
|
|
74
|
+
main.add_command(logs)
|
|
75
|
+
main.add_command(update)
|
|
76
|
+
|
|
77
|
+
# Is it reachable from the rest of the observatory, and if not, why not.
|
|
78
|
+
main.add_command(check_cmd)
|
|
79
|
+
|
|
80
|
+
# The access token Arcsecond gave the observatory, once per machine.
|
|
81
|
+
main.add_command(token_group)
|
|
60
82
|
|
|
61
83
|
# Browse and restore Arcsecond.local DB backups.
|
|
62
84
|
main.add_command(backups)
|
|
@@ -73,12 +95,14 @@ main.add_command(imagesources.webcam)
|
|
|
73
95
|
main.add_command(imagesources.allsky)
|
|
74
96
|
main.add_command(imagesources.proxy)
|
|
75
97
|
|
|
76
|
-
# Removed: a network camera is a webcam. Kept only to say so — see commands.py.
|
|
77
|
-
main.add_command(imagesources.netcam)
|
|
78
|
-
|
|
79
98
|
# Local ASCOM Alpaca diagnostics (e.g. `arcsecond alpaca probe dome ...`).
|
|
80
99
|
main.add_command(alpaca_group)
|
|
81
100
|
|
|
101
|
+
# The tool describing itself: `arcsecond docs commands --out DIR` writes the
|
|
102
|
+
# command reference the documentation site publishes. Hidden — it is for the
|
|
103
|
+
# documentation build, not for operators.
|
|
104
|
+
main.add_command(docs)
|
|
105
|
+
|
|
82
106
|
|
|
83
107
|
# `arcsecond proxy start` launches its detached proxy as
|
|
84
108
|
# `python -m arcsecond.cli proxy start --foreground`, which needs this. Going
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
from .auth import api, login
|
|
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
|
|
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
|
+
)
|