arcsecond 3.8.0__tar.gz → 3.9.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 (117) hide show
  1. arcsecond-3.9.0/.github/workflows/docsdeploy.yml +28 -0
  2. {arcsecond-3.8.0 → arcsecond-3.9.0}/.github/workflows/pythonpublish.yml +3 -3
  3. {arcsecond-3.8.0 → arcsecond-3.9.0}/.github/workflows/tests.yml +8 -8
  4. {arcsecond-3.8.0 → arcsecond-3.9.0}/.gitignore +1 -0
  5. {arcsecond-3.8.0 → arcsecond-3.9.0}/PKG-INFO +4 -3
  6. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/api/resources.py +73 -38
  7. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/docker/docker-compose.yml +0 -2
  8. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/local.py +2 -6
  9. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/webcam/proxy.py +12 -2
  10. {arcsecond-3.8.0 → arcsecond-3.9.0}/docs/.vitepress/config.js +15 -3
  11. arcsecond-3.9.0/docs/.vitepress/theme/custom.css +35 -0
  12. {arcsecond-3.8.0 → arcsecond-3.9.0}/docs/api-basics.md +11 -5
  13. arcsecond-3.9.0/docs/img/icon-logo.png +0 -0
  14. arcsecond-3.9.0/docs/index.md +36 -0
  15. arcsecond-3.9.0/docs/install.md +78 -0
  16. arcsecond-3.9.0/docs/public/img/icon-logo.png +0 -0
  17. {arcsecond-3.8.0 → arcsecond-3.9.0}/docs/resources.md +52 -28
  18. arcsecond-3.9.0/docs/upload.md +128 -0
  19. arcsecond-3.9.0/docs/webcam.md +116 -0
  20. arcsecond-3.9.0/package-lock.json +3528 -0
  21. {arcsecond-3.8.0 → arcsecond-3.9.0}/package.json +5 -3
  22. {arcsecond-3.8.0 → arcsecond-3.9.0}/pyproject.toml +7 -4
  23. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/api/test_targets.py +58 -11
  24. arcsecond-3.8.0/.github/workflows/docsdeploy.yml +0 -30
  25. arcsecond-3.8.0/docs/.vitepress/theme/custom.css +0 -4
  26. arcsecond-3.8.0/docs/img/logo-circle.png +0 -0
  27. arcsecond-3.8.0/docs/index.md +0 -156
  28. arcsecond-3.8.0/docs/install.md +0 -67
  29. arcsecond-3.8.0/package-lock.json +0 -2627
  30. {arcsecond-3.8.0 → arcsecond-3.9.0}/.docker/Dockerfile_postgres +0 -0
  31. {arcsecond-3.8.0 → arcsecond-3.9.0}/.docker/Dockerfile_redis +0 -0
  32. {arcsecond-3.8.0 → arcsecond-3.9.0}/.github/dependabot.yml +0 -0
  33. {arcsecond-3.8.0 → arcsecond-3.9.0}/LICENSE +0 -0
  34. {arcsecond-3.8.0 → arcsecond-3.9.0}/Makefile +0 -0
  35. {arcsecond-3.8.0 → arcsecond-3.9.0}/README.md +0 -0
  36. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/__init__.py +0 -0
  37. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/__version__.py +0 -0
  38. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/api/__init__.py +0 -0
  39. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/api/config.py +0 -0
  40. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/api/constants.py +0 -0
  41. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/api/endpoint.py +0 -0
  42. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/api/main.py +0 -0
  43. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cli.py +0 -0
  44. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/__init__.py +0 -0
  45. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/auth.py +0 -0
  46. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/resources.py +0 -0
  47. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/__init__.py +0 -0
  48. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/allskycameraimages/__init__.py +0 -0
  49. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/allskycameraimages/context.py +0 -0
  50. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/allskycameraimages/errors.py +0 -0
  51. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/allskycameraimages/uploader.py +0 -0
  52. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/allskycameraimages/utils.py +0 -0
  53. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/constants.py +0 -0
  54. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/context.py +0 -0
  55. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
  56. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
  57. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
  58. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
  59. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
  60. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/errors.py +0 -0
  61. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/logger.py +0 -0
  62. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/uploader.py +0 -0
  63. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/utils.py +0 -0
  64. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploader/walker.py +0 -0
  65. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/cloud/uploads.py +0 -0
  66. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/errors.py +0 -0
  67. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/__init__.py +0 -0
  68. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/checks.py +0 -0
  69. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/constants.py +0 -0
  70. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/docker/__init__.py +0 -0
  71. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/docker/constants.py +0 -0
  72. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/docker/containers.py +0 -0
  73. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/docker/images.py +0 -0
  74. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/docker/utils.py +0 -0
  75. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/keygen/__init__.py +0 -0
  76. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/keygen/client.py +0 -0
  77. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/keygen/utils.py +0 -0
  78. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/main.py +0 -0
  79. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/postgres/init-db.sh +0 -0
  80. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/setup.py +0 -0
  81. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/utils.py +0 -0
  82. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/hosting/validation.py +0 -0
  83. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/options.py +0 -0
  84. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/targets.py +0 -0
  85. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/webcam/__init__.py +0 -0
  86. {arcsecond-3.8.0 → arcsecond-3.9.0}/arcsecond/webcam/commands.py +0 -0
  87. {arcsecond-3.8.0 → arcsecond-3.9.0}/deploy.sh +0 -0
  88. {arcsecond-3.8.0 → arcsecond-3.9.0}/docs/.vitepress/theme/index.js +0 -0
  89. {arcsecond-3.8.0 → arcsecond-3.9.0}/examples/example_upload_files.py +0 -0
  90. {arcsecond-3.8.0 → arcsecond-3.9.0}/examples/example_upload_images.py +0 -0
  91. {arcsecond-3.8.0 → arcsecond-3.9.0}/poetry.lock +0 -0
  92. {arcsecond-3.8.0 → arcsecond-3.9.0}/requirements.txt +0 -0
  93. {arcsecond-3.8.0 → arcsecond-3.9.0}/setup.cfg +0 -0
  94. {arcsecond-3.8.0 → arcsecond-3.9.0}/setup.py +0 -0
  95. {arcsecond-3.8.0 → arcsecond-3.9.0}/sonar-project.properties +0 -0
  96. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/__init__.py +0 -0
  97. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/api/__init__.py +0 -0
  98. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/api/test_api.py +0 -0
  99. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/api/test_api_endpoint.py +0 -0
  100. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/api/test_config.py +0 -0
  101. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/__init__.py +0 -0
  102. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/__init__.py +0 -0
  103. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/allskycameraimages/__init__.py +0 -0
  104. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/allskycameraimages/test_context.py +0 -0
  105. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/allskycameraimages/test_uploader_full_process.py +0 -0
  106. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/datafiles/__init__.py +0 -0
  107. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
  108. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
  109. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
  110. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
  111. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
  112. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/conftest.py +0 -0
  113. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/fixtures/file1.fits +0 -0
  114. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/test_cli.py +0 -0
  115. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/test_hosting_local.py +0 -0
  116. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/test_targets_planning.py +0 -0
  117. {arcsecond-3.8.0 → arcsecond-3.9.0}/tests/utils.py +0 -0
@@ -0,0 +1,28 @@
1
+ name: Deploy Docs
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - name: Setup node
17
+ uses: actions/setup-node@v5
18
+ with:
19
+ node-version: '24'
20
+ cache: 'npm'
21
+ - run: npm ci
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{ secrets.READ_PACKAGE_TOKEN }}
24
+ CI: true
25
+ - name: Deploy docs
26
+ env:
27
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28
+ run: ./deploy.sh
@@ -22,7 +22,7 @@ jobs:
22
22
  exit 1
23
23
  fi
24
24
 
25
- - uses: actions/checkout@v4
25
+ - uses: actions/checkout@v5
26
26
 
27
27
  - name: Validate pyproject.toml version matches tag
28
28
  run: |
@@ -41,7 +41,7 @@ jobs:
41
41
  PY
42
42
 
43
43
  - name: Set up Python
44
- uses: actions/setup-python@v5
44
+ uses: actions/setup-python@v6
45
45
  with:
46
46
  python-version: '3.12'
47
47
 
@@ -54,4 +54,4 @@ jobs:
54
54
  - name: Publish to PyPI (OIDC)
55
55
  uses: pypa/gh-action-pypi-publish@release/v1
56
56
  with:
57
- packages-dir: dist
57
+ packages-dir: dist
@@ -14,10 +14,10 @@ jobs:
14
14
  python-version: [ '3.10', '3.11', '3.12', '3.13' ]
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v4
17
+ - uses: actions/checkout@v5
18
18
 
19
19
  - name: Set up Python ${{ matrix.python-version }}
20
- uses: actions/setup-python@v4
20
+ uses: actions/setup-python@v6
21
21
  with:
22
22
  python-version: ${{ matrix.python-version }}
23
23
 
@@ -39,7 +39,7 @@ jobs:
39
39
 
40
40
  - name: Upload coverage artifact (only on 3.12)
41
41
  if: matrix.python-version == '3.12'
42
- uses: actions/upload-artifact@v4
42
+ uses: actions/upload-artifact@v6
43
43
  with:
44
44
  name: coverage
45
45
  path: coverage.xml
@@ -49,10 +49,10 @@ jobs:
49
49
  needs: tests
50
50
 
51
51
  steps:
52
- - uses: actions/checkout@v4
52
+ - uses: actions/checkout@v5
53
53
 
54
54
  - name: Download coverage artifact
55
- uses: actions/download-artifact@v4
55
+ uses: actions/download-artifact@v5
56
56
  with:
57
57
  name: coverage
58
58
  path: .
@@ -65,10 +65,10 @@ jobs:
65
65
  lint:
66
66
  runs-on: ubuntu-latest
67
67
  steps:
68
- - uses: actions/checkout@v4
68
+ - uses: actions/checkout@v5
69
69
 
70
70
  - name: Set up Python
71
- uses: actions/setup-python@v4
71
+ uses: actions/setup-python@v6
72
72
  with:
73
73
  python-version: '3.12'
74
74
 
@@ -87,4 +87,4 @@ jobs:
87
87
 
88
88
  - name: Check imports with isort
89
89
  run: |
90
- isort --check-only --profile black arcsecond tests
90
+ isort --check-only --profile black arcsecond tests
@@ -20,3 +20,4 @@ node_modules
20
20
 
21
21
  /coverage.xml
22
22
  .env
23
+ .claude
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arcsecond
3
- Version: 3.8.0
3
+ Version: 3.9.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,15 +36,16 @@ 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: aiohttp>=3.9
40
39
  Requires-Dist: click>=8
41
40
  Requires-Dist: configparser
42
41
  Requires-Dist: docker
43
42
  Requires-Dist: httpx
44
- Requires-Dist: opencv-python-headless<5,>=4.10
45
43
  Requires-Dist: py-machineid
46
44
  Requires-Dist: tqdm<5.0.0,>=4.67.1
47
45
  Requires-Dist: wait-for-it
46
+ Provides-Extra: webcam
47
+ Requires-Dist: aiohttp>=3.9; extra == 'webcam'
48
+ Requires-Dist: opencv-python-headless<5,>=4.10; extra == 'webcam'
48
49
  Description-Content-Type: text/markdown
49
50
 
50
51
  [![Deploy Docs](https://github.com/arcsecond-io/cli/actions/workflows/docsdeploy.yml/badge.svg)](https://github.com/arcsecond-io/cli/actions/workflows/docsdeploy.yml) [![Downloads](http://pepy.tech/badge/arcsecond)](http://pepy.tech/project/arcsecond)
@@ -6,53 +6,76 @@ from .endpoint import ArcsecondAPIEndpoint
6
6
  class ArcsecondTargetListsResource(ArcsecondAPIEndpoint):
7
7
  """Target-list specific helpers built on top of the generic endpoint contract."""
8
8
 
9
- target_relation_keys = ("targets", "target_uuids", "target_ids")
9
+ target_relation_key = "targets"
10
+ target_writable_fields = (
11
+ "id",
12
+ "pk",
13
+ "object",
14
+ "name",
15
+ "identifier",
16
+ "target_class",
17
+ "mode",
18
+ "color",
19
+ "notes",
20
+ "tags",
21
+ "profile",
22
+ "organisation",
23
+ )
10
24
 
11
25
  def _ensure_iterable(self, values):
12
26
  if values is None:
13
27
  return None
28
+ if isinstance(values, dict):
29
+ return [values]
14
30
  if isinstance(values, (str, int)):
15
31
  return [values]
16
32
  return list(values)
17
33
 
18
- def _normalise_target_references(self, targets):
34
+ def _target_payload_identity(self, target):
35
+ if target.get("id") is not None:
36
+ return ("id", target["id"])
37
+ if target.get("pk") is not None:
38
+ return ("pk", target["pk"])
39
+ return (
40
+ "composite",
41
+ target.get("target_class"),
42
+ target.get("identifier"),
43
+ target.get("name"),
44
+ target.get("mode"),
45
+ )
46
+
47
+ def _normalise_target_payloads(self, targets):
19
48
  values = self._ensure_iterable(targets)
20
49
  if values is None:
21
50
  return None
22
51
 
23
- refs = []
52
+ payloads = []
24
53
  for target in values:
25
- if isinstance(target, dict):
26
- ref = (
27
- target.get("uuid")
28
- or target.get("id")
29
- or target.get("pk")
30
- or target.get("name")
54
+ if not isinstance(target, dict):
55
+ raise ArcsecondError(
56
+ "Target list helpers expect target payload dictionaries, not scalar IDs or UUIDs. "
57
+ "Pass dictionaries such as `plan_target_payload(...).payload` or target objects returned "
58
+ "by `api.targets.read()/list()/upsert()`."
31
59
  )
32
- if ref is None:
33
- raise ArcsecondError(
34
- "Target dictionaries must include one of: uuid, id, pk or name."
35
- )
36
- refs.append(ref)
37
- else:
38
- refs.append(target)
39
- return refs
40
-
41
- def _target_key_from_payload(self, payload, target_key=None):
42
- if target_key:
43
- return target_key
44
- for key in self.target_relation_keys:
45
- if payload and key in payload:
46
- return key
47
- return self.target_relation_keys[0]
60
+
61
+ payload = {
62
+ key: value
63
+ for key, value in target.items()
64
+ if key in self.target_writable_fields and value is not None
65
+ }
66
+ if not payload:
67
+ raise ArcsecondError(
68
+ "Target dictionaries must include at least one writable target field."
69
+ )
70
+
71
+ payloads.append(payload)
72
+ return payloads
48
73
 
49
74
  def _build_payload(self, json=None, targets=None, target_key=None, **fields):
50
75
  payload = super()._build_payload(json=json, **fields) or {}
51
- normalised_targets = self._normalise_target_references(targets)
76
+ normalised_targets = self._normalise_target_payloads(targets)
52
77
  if normalised_targets is not None:
53
- payload[self._target_key_from_payload(payload, target_key=target_key)] = (
54
- normalised_targets
55
- )
78
+ payload[target_key or self.target_relation_key] = normalised_targets
56
79
  return payload or None
57
80
 
58
81
  def create(self, json=None, targets=None, target_key=None, **fields):
@@ -74,14 +97,14 @@ class ArcsecondTargetListsResource(ArcsecondAPIEndpoint):
74
97
  return super().upsert(match_field=match_field, json=payload)
75
98
 
76
99
  def _read_target_refs(self, target_list, target_key=None):
77
- key = self._target_key_from_payload(target_list or {}, target_key=target_key)
100
+ key = target_key or self.target_relation_key
78
101
  raw_targets = (target_list or {}).get(key, [])
79
- refs = self._normalise_target_references(raw_targets) or []
102
+ refs = self._normalise_target_payloads(raw_targets) or []
80
103
  return key, refs
81
104
 
82
105
  def set_targets(self, id_name_uuid, targets, target_key=None):
83
- target_key = self._target_key_from_payload({}, target_key=target_key)
84
- return self.update(id_name_uuid, **{target_key: self._normalise_target_references(targets)})
106
+ target_key = target_key or self.target_relation_key
107
+ return self.update(id_name_uuid, **{target_key: self._normalise_target_payloads(targets)})
85
108
 
86
109
  def clear_targets(self, id_name_uuid, target_key=None):
87
110
  return self.set_targets(id_name_uuid, [], target_key=target_key)
@@ -92,9 +115,14 @@ class ArcsecondTargetListsResource(ArcsecondAPIEndpoint):
92
115
  return None, error
93
116
 
94
117
  key, current_refs = self._read_target_refs(target_list, target_key=target_key)
95
- for ref in self._normalise_target_references(targets) or []:
96
- if ref not in current_refs:
97
- current_refs.append(ref)
118
+ current_identities = {
119
+ self._target_payload_identity(target): target for target in current_refs
120
+ }
121
+ for target in self._normalise_target_payloads(targets) or []:
122
+ identity = self._target_payload_identity(target)
123
+ if identity not in current_identities:
124
+ current_refs.append(target)
125
+ current_identities[identity] = target
98
126
  return self.update(id_name_uuid, **{key: current_refs})
99
127
 
100
128
  def remove_targets(self, id_name_uuid, targets, target_key=None):
@@ -103,8 +131,15 @@ class ArcsecondTargetListsResource(ArcsecondAPIEndpoint):
103
131
  return None, error
104
132
 
105
133
  key, current_refs = self._read_target_refs(target_list, target_key=target_key)
106
- refs_to_remove = set(self._normalise_target_references(targets) or [])
107
- remaining_refs = [ref for ref in current_refs if ref not in refs_to_remove]
134
+ refs_to_remove = {
135
+ self._target_payload_identity(target)
136
+ for target in (self._normalise_target_payloads(targets) or [])
137
+ }
138
+ remaining_refs = [
139
+ ref
140
+ for ref in current_refs
141
+ if self._target_payload_identity(ref) not in refs_to_remove
142
+ ]
108
143
  return self.update(id_name_uuid, **{key: remaining_refs})
109
144
 
110
145
  def add_target(self, id_name_uuid, target, target_key=None):
@@ -37,8 +37,6 @@ services:
37
37
  - broker
38
38
  # Allows the backend to reach the host machine via host.docker.internal.
39
39
  # Required on Linux; Docker Desktop on Windows/macOS adds this automatically.
40
- # Used by the webcam proxy: set WEBCAM_PROXY_URL=http://host.docker.internal:8765
41
- # in your .env file and run `arcsecond webcam start` on the host.
42
40
  extra_hosts:
43
41
  - "host.docker.internal:host-gateway"
44
42
  env_file:
@@ -125,12 +125,8 @@ def write_docker_compose_file() -> Path:
125
125
  print("docker-compose.yml is already up to date.")
126
126
  return dest
127
127
 
128
- latest_dest = Path.cwd() / "docker-compose.latest.yml"
129
- latest_dest.write_bytes(expected_content)
130
- print(
131
- "docker-compose.yml differs from the latest packaged version. "
132
- f"Wrote new template to: {latest_dest}"
133
- )
128
+ dest.write_bytes(expected_content)
129
+ print("docker-compose.yml has been updated to the latest version.")
134
130
  return dest
135
131
 
136
132
 
@@ -68,10 +68,16 @@ def _detect_webcams_sync(max_index: int = _MAX_PROBE) -> list[WebcamInfo]:
68
68
  # aiohttp request handlers
69
69
  # ---------------------------------------------------------------------------
70
70
 
71
+ async def handle_health(request):
72
+ """GET /health — simple liveness check."""
73
+ from aiohttp import web
74
+ return web.json_response({'status': 'ok'})
75
+
76
+
71
77
  async def handle_detect(request):
72
78
  """GET /detect — return JSON list of attached webcams."""
73
79
  from aiohttp import web
74
- loop = asyncio.get_event_loop()
80
+ loop = asyncio.get_running_loop()
75
81
  webcams = await loop.run_in_executor(None, _detect_webcams_sync)
76
82
  return web.json_response([asdict(w) for w in webcams])
77
83
 
@@ -86,7 +92,7 @@ async def handle_stream(request):
86
92
  await ws.prepare(request)
87
93
  logger.info("Webcam proxy: client connected to stream for device index %d", index)
88
94
 
89
- loop = asyncio.get_event_loop()
95
+ loop = asyncio.get_running_loop()
90
96
  cap: cv2.VideoCapture = await loop.run_in_executor(None, cv2.VideoCapture, index)
91
97
 
92
98
  if not await loop.run_in_executor(None, cap.isOpened):
@@ -109,9 +115,12 @@ async def handle_stream(request):
109
115
  b64 = await loop.run_in_executor(None, _read_and_encode)
110
116
  if b64 is None:
111
117
  logger.warning("Webcam proxy: cap.read() failed for device %d, stopping.", index)
118
+ await ws.send_str(json.dumps({'type': 'error', 'message': f'Device lost at index {index}.'}))
112
119
  break
113
120
  await ws.send_str(json.dumps({'type': 'frame', 'format': 'jpeg/base64', 'data': b64}))
114
121
  await asyncio.sleep(_FRAME_INTERVAL)
122
+ except (ConnectionResetError, ConnectionError):
123
+ logger.info("Webcam proxy: client disconnected from device %d.", index)
115
124
  finally:
116
125
  await loop.run_in_executor(None, cap.release)
117
126
  logger.info("Webcam proxy: device %d released.", index)
@@ -128,6 +137,7 @@ def run(host: str = '0.0.0.0', port: int = 8765):
128
137
  from aiohttp import web
129
138
 
130
139
  app = web.Application()
140
+ app.router.add_get('/health', handle_health)
131
141
  app.router.add_get('/detect', handle_detect)
132
142
  app.router.add_get('/stream/{index}', handle_stream)
133
143
 
@@ -2,16 +2,28 @@ module.exports = {
2
2
  title: 'Arcsecond CLI',
3
3
  description: 'The command-line / Python module of Arcsecond.',
4
4
  base: '/cli/',
5
+ cleanUrls: true,
5
6
  themeConfig: {
6
7
  nav: [
7
- { text: 'Home', link: '/' },
8
8
  { text: 'Arcsecond Docs', link: 'https://docs.arcsecond.io' }
9
9
  ],
10
10
  sidebar: [
11
11
  {
12
- text: 'Guide',
12
+ text: 'Getting Started',
13
+ items: [
14
+ { text: 'Install & Login', link: '/install' },
15
+ { text: 'Data Upload', link: '/upload' }
16
+ ]
17
+ },
18
+ {
19
+ text: 'Self-Hosting',
20
+ items: [
21
+ { text: 'Webcam Proxy', link: '/webcam' }
22
+ ]
23
+ },
24
+ {
25
+ text: 'Python API',
13
26
  items: [
14
- { text: 'Install', link: '/install' },
15
27
  { text: 'API Basics', link: '/api-basics' },
16
28
  { text: 'Resources', link: '/resources' }
17
29
  ]
@@ -0,0 +1,35 @@
1
+ :root {
2
+ --vp-c-brand-1: #081c4d;
3
+ --vp-c-brand-2: #0b2462;
4
+ --vp-c-brand-3: #0e2d77;
5
+ --vp-c-brand-soft: rgba(8, 28, 77, 0.14);
6
+ }
7
+
8
+ .dark {
9
+ --vp-c-brand-1: #6ba6dc;
10
+ --vp-c-brand-2: #82b4e3;
11
+ --vp-c-brand-3: #528fc8;
12
+ --vp-c-brand-soft: rgba(107, 166, 220, 0.18);
13
+ }
14
+
15
+ .VPHomeHero .image-container {
16
+ width: 220px;
17
+ height: 220px;
18
+ }
19
+
20
+ .VPHomeHero .image-src {
21
+ max-width: 120px;
22
+ max-height: 120px;
23
+ }
24
+
25
+ @media (min-width: 640px) {
26
+ .VPHomeHero .image-container {
27
+ width: 260px;
28
+ height: 260px;
29
+ }
30
+
31
+ .VPHomeHero .image-src {
32
+ max-width: 120px;
33
+ max-height: 120px;
34
+ }
35
+ }
@@ -25,7 +25,7 @@ api = ArcsecondAPI(config, subdomain="my-observatory")
25
25
 
26
26
  Authentication with the Python module currently relies on your Arcsecond keys.
27
27
 
28
- ### With the CLI
28
+ ### Reuse CLI credentials
29
29
 
30
30
  Login once from the command line:
31
31
 
@@ -33,18 +33,19 @@ Login once from the command line:
33
33
  arcsecond login
34
34
  ```
35
35
 
36
- This stores your credentials locally in `~/.config/arcsecond/config.ini`.
36
+ This stores your credentials locally in `~/.config/arcsecond/config.ini`, which
37
+ `ArcsecondConfig()` will load automatically.
37
38
 
38
39
  To skip prompts:
39
40
 
40
41
  ```bash
41
- arcsecond login --username <username> --access_key <key>
42
+ arcsecond login --username <username> --type access --key <access-key>
42
43
  ```
43
44
 
44
45
  or:
45
46
 
46
47
  ```bash
47
- arcsecond login --username <username> --upload_key <key>
48
+ arcsecond login --username <username> --type upload --key <upload-key>
48
49
  ```
49
50
 
50
51
  Once that is done, Python code can reuse the stored configuration:
@@ -56,7 +57,7 @@ config = ArcsecondConfig()
56
57
  api = ArcsecondAPI(config)
57
58
  ```
58
59
 
59
- ### In Python Code
60
+ ### Authenticate in Python code
60
61
 
61
62
  You can also authenticate directly in Python code:
62
63
 
@@ -89,3 +90,8 @@ backend or local automation contexts, never in browser-side code.
89
90
 
90
91
  For Python scripts that only need to upload data, prefer an Upload Key. For broader
91
92
  resource management, use an Access Key.
93
+
94
+ ## Next Step
95
+
96
+ Once authenticated, move to [Resources](/resources) for the generic CRUD helpers,
97
+ target planning utilities, and target list management helpers.
Binary file
@@ -0,0 +1,36 @@
1
+ ---
2
+ layout: home
3
+ title: Arcsecond CLI
4
+ description: Arcsecond CLI
5
+ hero:
6
+ name: Arcsecond CLI
7
+ tagline: A lightweight command-line and Python client for Arcsecond resources, target workflows, and data uploads.
8
+ image:
9
+ src: /img/icon-logo.png
10
+ alt: Arcsecond CLI
11
+ actions:
12
+ - theme: brand
13
+ text: Install & Login
14
+ link: /install
15
+ - theme: alt
16
+ text: Python API
17
+ link: /api-basics
18
+ - theme: alt
19
+ text: Resources
20
+ link: /resources
21
+ features:
22
+ - title: Lightweight CLI
23
+ details: Install the package, log in once, and use simple commands for resource access and uploads.
24
+ - title: Python API
25
+ details: Reuse the same credentials in code, work with generic CRUD endpoints, and plan target payloads safely.
26
+ - title: Data Upload
27
+ details: Upload datasets and all-sky camera images from the CLI or from Python automation.
28
+ footer: MIT Licensed | Copyright © 2018-present Arcsecond.io (F52 Tech).
29
+ ---
30
+
31
+ Arcsecond CLI gives you two entry points:
32
+
33
+ - the `arcsecond` command-line tool for day-to-day operations
34
+ - the `arcsecond` Python module for automation and integration
35
+
36
+ Use the sidebar to move directly to installation, authentication, uploads, and the Python resource helpers.
@@ -0,0 +1,78 @@
1
+ ---
2
+ sidebar: true
3
+ ---
4
+
5
+ # Install & Login
6
+
7
+ Simply issue the following in a Terminal:
8
+
9
+ ```bash
10
+ pip install arcsecond
11
+ ```
12
+
13
+ To upgrade an existing Arcsecond installation:
14
+
15
+ ```bash
16
+ pip install --upgrade arcsecond
17
+ ```
18
+
19
+ The help is available like any other command-line tool:
20
+
21
+ ```bash
22
+ arcsecond --help
23
+ ```
24
+
25
+ For subcommands:
26
+
27
+ ```bash
28
+ arcsecond <command> --help
29
+ ```
30
+
31
+ The Arcsecond CLI works like a tool such as `git`: `arcsecond` is the main entry
32
+ point, followed by a command. Many commands directly map to Arcsecond resources.
33
+
34
+ ## Authentication
35
+
36
+ To use the CLI, you need an Arcsecond account. In your settings page on
37
+ [arcsecond.io](https://www.arcsecond.io) you will find two kinds of credentials:
38
+
39
+ - an Access Key for broad access to your resources
40
+ - an Upload Key for upload-only workflows
41
+
42
+ Use an Access Key only on trusted computers. If you only need to upload files,
43
+ prefer an Upload Key.
44
+
45
+ ### Interactive login
46
+
47
+ Run:
48
+
49
+ ```bash
50
+ arcsecond login
51
+ ```
52
+
53
+ The CLI will prompt for:
54
+
55
+ - `username`
56
+ - `type` (`access` or `upload`)
57
+ - `key`
58
+
59
+ Your credential is stored locally in `~/.config/arcsecond/config.ini`.
60
+
61
+ ### Non-interactive login
62
+
63
+ To skip prompts, pass all values explicitly:
64
+
65
+ ```bash
66
+ arcsecond login --username <username> --type access --key <access-key>
67
+ ```
68
+
69
+ or:
70
+
71
+ ```bash
72
+ arcsecond login --username <username> --type upload --key <upload-key>
73
+ ```
74
+
75
+ Logging in again overwrites the stored credential if the login succeeds.
76
+
77
+ If you think a key is compromised, regenerate it from your profile settings on
78
+ [arcsecond.io](https://www.arcsecond.io). The CLI cannot regenerate keys for you.