arcsecond 3.7.3__tar.gz → 3.8.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-3.8.1/.github/workflows/docsdeploy.yml +28 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/.github/workflows/pythonpublish.yml +3 -3
- {arcsecond-3.7.3 → arcsecond-3.8.1}/.github/workflows/tests.yml +8 -8
- {arcsecond-3.7.3 → arcsecond-3.8.1}/PKG-INFO +3 -1
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/__init__.py +10 -1
- arcsecond-3.8.1/arcsecond/api/__init__.py +11 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/api/endpoint.py +77 -4
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/api/main.py +5 -0
- arcsecond-3.8.1/arcsecond/api/resources.py +149 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cli.py +4 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/docker/docker-compose.yml +6 -0
- arcsecond-3.8.1/arcsecond/targets.py +211 -0
- arcsecond-3.8.1/arcsecond/webcam/commands.py +101 -0
- arcsecond-3.8.1/arcsecond/webcam/proxy.py +135 -0
- arcsecond-3.8.1/docs/.vitepress/config.js +27 -0
- arcsecond-3.8.1/docs/api-basics.md +97 -0
- arcsecond-3.8.1/docs/img/icon-logo.png +0 -0
- arcsecond-3.8.1/docs/index.md +36 -0
- arcsecond-3.8.1/docs/install.md +78 -0
- arcsecond-3.8.1/docs/public/img/icon-logo.png +0 -0
- arcsecond-3.8.1/docs/resources.md +359 -0
- arcsecond-3.8.1/docs/upload.md +128 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/package-lock.json +2 -2
- {arcsecond-3.7.3 → arcsecond-3.8.1}/package.json +2 -2
- {arcsecond-3.7.3 → arcsecond-3.8.1}/pyproject.toml +6 -1
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/api/test_api_endpoint.py +50 -0
- arcsecond-3.8.1/tests/api/test_targets.py +243 -0
- arcsecond-3.8.1/tests/cloud/uploader/datafiles/__init__.py +0 -0
- arcsecond-3.8.1/tests/test_targets_planning.py +86 -0
- arcsecond-3.7.3/.github/workflows/docsdeploy.yml +0 -30
- arcsecond-3.7.3/arcsecond/api/__init__.py +0 -5
- arcsecond-3.7.3/docs/.vitepress/config.js +0 -13
- arcsecond-3.7.3/docs/img/logo-circle.png +0 -0
- arcsecond-3.7.3/docs/index.md +0 -156
- arcsecond-3.7.3/docs/install.md +0 -67
- {arcsecond-3.7.3 → arcsecond-3.8.1}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/.docker/Dockerfile_redis +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/.github/dependabot.yml +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/.gitignore +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/LICENSE +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/Makefile +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/README.md +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/__version__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/api/config.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/api/constants.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/auth.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/allskycameraimages/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/allskycameraimages/context.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/allskycameraimages/errors.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/allskycameraimages/uploader.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/allskycameraimages/utils.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/cloud/uploads.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/errors.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/checks.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/constants.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/docker/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/docker/constants.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/docker/containers.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/docker/images.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/docker/utils.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/keygen/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/keygen/client.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/keygen/utils.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/local.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/main.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/postgres/init-db.sh +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/setup.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/utils.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/hosting/validation.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/arcsecond/options.py +0 -0
- {arcsecond-3.7.3/tests → arcsecond-3.8.1/arcsecond/webcam}/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/deploy.sh +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/docs/.vitepress/theme/custom.css +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/docs/.vitepress/theme/index.js +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/examples/example_upload_files.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/examples/example_upload_images.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/poetry.lock +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/requirements.txt +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/setup.cfg +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/setup.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/sonar-project.properties +0 -0
- {arcsecond-3.7.3/tests/api → arcsecond-3.8.1/tests}/__init__.py +0 -0
- {arcsecond-3.7.3/tests/cloud → arcsecond-3.8.1/tests/api}/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/api/test_api.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/api/test_config.py +0 -0
- {arcsecond-3.7.3/tests/cloud/uploader → arcsecond-3.8.1/tests/cloud}/__init__.py +0 -0
- {arcsecond-3.7.3/tests/cloud/uploader/allskycameraimages → arcsecond-3.8.1/tests/cloud/uploader}/__init__.py +0 -0
- {arcsecond-3.7.3/tests/cloud/uploader/datafiles → arcsecond-3.8.1/tests/cloud/uploader/allskycameraimages}/__init__.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/allskycameraimages/test_context.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/allskycameraimages/test_uploader_full_process.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/conftest.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/fixtures/file1.fits +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/test_cli.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/tests/test_hosting_local.py +0 -0
- {arcsecond-3.7.3 → arcsecond-3.8.1}/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@
|
|
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@
|
|
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@
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
18
|
|
|
19
19
|
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
-
uses: actions/setup-python@
|
|
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@
|
|
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@
|
|
52
|
+
- uses: actions/checkout@v5
|
|
53
53
|
|
|
54
54
|
- name: Download coverage artifact
|
|
55
|
-
uses: actions/download-artifact@
|
|
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@
|
|
68
|
+
- uses: actions/checkout@v5
|
|
69
69
|
|
|
70
70
|
- name: Set up Python
|
|
71
|
-
uses: actions/setup-python@
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arcsecond
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.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
|
|
@@ -36,10 +36,12 @@ 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
|
|
39
40
|
Requires-Dist: click>=8
|
|
40
41
|
Requires-Dist: configparser
|
|
41
42
|
Requires-Dist: docker
|
|
42
43
|
Requires-Dist: httpx
|
|
44
|
+
Requires-Dist: opencv-python-headless<5,>=4.10
|
|
43
45
|
Requires-Dist: py-machineid
|
|
44
46
|
Requires-Dist: tqdm<5.0.0,>=4.67.1
|
|
45
47
|
Requires-Dist: wait-for-it
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from .api import
|
|
1
|
+
from .api import (
|
|
2
|
+
ArcsecondAPI,
|
|
3
|
+
ArcsecondAPIEndpoint,
|
|
4
|
+
ArcsecondConfig,
|
|
5
|
+
ArcsecondTargetListsResource,
|
|
6
|
+
)
|
|
2
7
|
from .cloud.uploader import (
|
|
3
8
|
AllSkyCameraImageFileUploader,
|
|
4
9
|
AllSkyCameraImageUploadContext,
|
|
@@ -7,6 +12,7 @@ from .cloud.uploader import (
|
|
|
7
12
|
)
|
|
8
13
|
from .cloud.uploader.walker import walk_folder_and_upload_files
|
|
9
14
|
from .errors import ArcsecondError
|
|
15
|
+
from .targets import ArcsecondTargetPayloadPlan, plan_target_payload
|
|
10
16
|
|
|
11
17
|
name = "arcsecond"
|
|
12
18
|
|
|
@@ -15,9 +21,12 @@ __all__ = [
|
|
|
15
21
|
"ArcsecondError",
|
|
16
22
|
"ArcsecondConfig",
|
|
17
23
|
"ArcsecondAPIEndpoint",
|
|
24
|
+
"ArcsecondTargetListsResource",
|
|
25
|
+
"ArcsecondTargetPayloadPlan",
|
|
18
26
|
"DatasetUploadContext",
|
|
19
27
|
"DatasetFileUploader",
|
|
20
28
|
"AllSkyCameraImageFileUploader",
|
|
21
29
|
"AllSkyCameraImageUploadContext",
|
|
30
|
+
"plan_target_payload",
|
|
22
31
|
"walk_folder_and_upload_files",
|
|
23
32
|
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .config import ArcsecondConfig
|
|
2
|
+
from .endpoint import ArcsecondAPIEndpoint
|
|
3
|
+
from .main import ArcsecondAPI
|
|
4
|
+
from .resources import ArcsecondTargetListsResource
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ArcsecondAPI",
|
|
8
|
+
"ArcsecondConfig",
|
|
9
|
+
"ArcsecondAPIEndpoint",
|
|
10
|
+
"ArcsecondTargetListsResource",
|
|
11
|
+
]
|
|
@@ -12,6 +12,13 @@ WRITABLE_MEMBERSHIPS = ["superadmin", "admin", "member"]
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class ArcsecondAPIEndpoint(object):
|
|
15
|
+
"""
|
|
16
|
+
Generic REST endpoint wrapper for Arcsecond resources.
|
|
17
|
+
|
|
18
|
+
It owns transport-level CRUD plus resource-agnostic conveniences such as
|
|
19
|
+
payload merging, `find_one()`, and `upsert()`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
15
22
|
def __init__(
|
|
16
23
|
self,
|
|
17
24
|
config: ArcsecondConfig,
|
|
@@ -57,6 +64,30 @@ class ArcsecondAPIEndpoint(object):
|
|
|
57
64
|
def _detail_url(self, uuid_or_id):
|
|
58
65
|
return self._build_url(self.__path, str(uuid_or_id))
|
|
59
66
|
|
|
67
|
+
def _build_payload(self, json=None, **fields):
|
|
68
|
+
payload = {}
|
|
69
|
+
if json:
|
|
70
|
+
payload.update(json)
|
|
71
|
+
payload.update({key: value for key, value in fields.items() if value is not None})
|
|
72
|
+
return payload or None
|
|
73
|
+
|
|
74
|
+
def _extract_results(self, response):
|
|
75
|
+
if isinstance(response, dict):
|
|
76
|
+
if isinstance(response.get("results"), list):
|
|
77
|
+
return response["results"]
|
|
78
|
+
if response:
|
|
79
|
+
return [response]
|
|
80
|
+
elif isinstance(response, list):
|
|
81
|
+
return response
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
def _extract_identifier(self, resource, identifier_fields=("uuid", "id", "pk")):
|
|
85
|
+
for key in identifier_fields:
|
|
86
|
+
value = resource.get(key)
|
|
87
|
+
if value is not None:
|
|
88
|
+
return value
|
|
89
|
+
return None
|
|
90
|
+
|
|
60
91
|
def list(self, **filters):
|
|
61
92
|
return self._perform_request(self._list_url(**filters), "get")
|
|
62
93
|
|
|
@@ -65,16 +96,20 @@ class ArcsecondAPIEndpoint(object):
|
|
|
65
96
|
self._detail_url(id_name_uuid), "get", headers=headers
|
|
66
97
|
)
|
|
67
98
|
|
|
68
|
-
def create(self, json=None, files=None, headers=None):
|
|
99
|
+
def create(self, json=None, files=None, headers=None, **fields):
|
|
69
100
|
return self._perform_request(
|
|
70
|
-
self._list_url(),
|
|
101
|
+
self._list_url(),
|
|
102
|
+
"post",
|
|
103
|
+
json=self._build_payload(json=json, **fields),
|
|
104
|
+
files=files,
|
|
105
|
+
headers=headers,
|
|
71
106
|
)
|
|
72
107
|
|
|
73
|
-
def update(self, id_name_uuid, json=None, files=None, headers=None):
|
|
108
|
+
def update(self, id_name_uuid, json=None, files=None, headers=None, **fields):
|
|
74
109
|
return self._perform_request(
|
|
75
110
|
self._detail_url(id_name_uuid),
|
|
76
111
|
"patch",
|
|
77
|
-
json=json,
|
|
112
|
+
json=self._build_payload(json=json, **fields),
|
|
78
113
|
files=files,
|
|
79
114
|
headers=headers,
|
|
80
115
|
)
|
|
@@ -82,6 +117,44 @@ class ArcsecondAPIEndpoint(object):
|
|
|
82
117
|
def delete(self, id_name_uuid):
|
|
83
118
|
return self._perform_request(self._detail_url(id_name_uuid), "delete")
|
|
84
119
|
|
|
120
|
+
def find_one(self, **filters):
|
|
121
|
+
response, error = self.list(**filters)
|
|
122
|
+
if error:
|
|
123
|
+
return None, error
|
|
124
|
+
|
|
125
|
+
results = self._extract_results(response)
|
|
126
|
+
if len(results) == 0:
|
|
127
|
+
return None, None
|
|
128
|
+
if len(results) > 1:
|
|
129
|
+
return (
|
|
130
|
+
None,
|
|
131
|
+
ArcsecondError(
|
|
132
|
+
f"Expected one '{self.path}' match for filters {filters}, got {len(results)}."
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
return results[0], None
|
|
136
|
+
|
|
137
|
+
def upsert(self, match_field="name", json=None, **fields):
|
|
138
|
+
payload = self._build_payload(json=json, **fields)
|
|
139
|
+
if payload is None:
|
|
140
|
+
return None, ArcsecondError("Cannot upsert an empty payload.")
|
|
141
|
+
|
|
142
|
+
match_value = payload.get(match_field)
|
|
143
|
+
if match_value in (None, ""):
|
|
144
|
+
return self.create(json=payload)
|
|
145
|
+
|
|
146
|
+
existing, error = self.find_one(**{match_field: match_value})
|
|
147
|
+
if error:
|
|
148
|
+
return None, error
|
|
149
|
+
if existing is None:
|
|
150
|
+
return self.create(json=payload)
|
|
151
|
+
|
|
152
|
+
identifier = self._extract_identifier(existing)
|
|
153
|
+
if identifier is None:
|
|
154
|
+
return None, ArcsecondError(f"Could not find an identifier for '{match_value}'.")
|
|
155
|
+
|
|
156
|
+
return self.update(identifier, json=payload)
|
|
157
|
+
|
|
85
158
|
def _perform_request(self, url, method_name, json=None, files=None, headers=None):
|
|
86
159
|
if self.__config.verbose:
|
|
87
160
|
click.echo(f"Sending {method_name} request to {url}")
|
|
@@ -8,6 +8,7 @@ from arcsecond.options import State
|
|
|
8
8
|
from .config import ArcsecondConfig
|
|
9
9
|
from .constants import API_AUTH_PATH_VERIFY
|
|
10
10
|
from .endpoint import ArcsecondAPIEndpoint
|
|
11
|
+
from .resources import ArcsecondTargetListsResource
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"ArcsecondAPI",
|
|
@@ -40,6 +41,10 @@ class ArcsecondAPI(object):
|
|
|
40
41
|
self.calibrations = ArcsecondAPIEndpoint(
|
|
41
42
|
self.config, "calibrations", self.subdomain
|
|
42
43
|
)
|
|
44
|
+
self.targets = ArcsecondAPIEndpoint(self.config, "targets", self.subdomain)
|
|
45
|
+
self.targetlists = ArcsecondTargetListsResource(
|
|
46
|
+
self.config, "targetlists", self.subdomain
|
|
47
|
+
)
|
|
43
48
|
|
|
44
49
|
self.datapackages = ArcsecondAPIEndpoint(
|
|
45
50
|
self.config, "datapackages", self.subdomain
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from arcsecond.errors import ArcsecondError
|
|
2
|
+
|
|
3
|
+
from .endpoint import ArcsecondAPIEndpoint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ArcsecondTargetListsResource(ArcsecondAPIEndpoint):
|
|
7
|
+
"""Target-list specific helpers built on top of the generic endpoint contract."""
|
|
8
|
+
|
|
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
|
+
)
|
|
24
|
+
|
|
25
|
+
def _ensure_iterable(self, values):
|
|
26
|
+
if values is None:
|
|
27
|
+
return None
|
|
28
|
+
if isinstance(values, dict):
|
|
29
|
+
return [values]
|
|
30
|
+
if isinstance(values, (str, int)):
|
|
31
|
+
return [values]
|
|
32
|
+
return list(values)
|
|
33
|
+
|
|
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):
|
|
48
|
+
values = self._ensure_iterable(targets)
|
|
49
|
+
if values is None:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
payloads = []
|
|
53
|
+
for target in values:
|
|
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()`."
|
|
59
|
+
)
|
|
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
|
|
73
|
+
|
|
74
|
+
def _build_payload(self, json=None, targets=None, target_key=None, **fields):
|
|
75
|
+
payload = super()._build_payload(json=json, **fields) or {}
|
|
76
|
+
normalised_targets = self._normalise_target_payloads(targets)
|
|
77
|
+
if normalised_targets is not None:
|
|
78
|
+
payload[target_key or self.target_relation_key] = normalised_targets
|
|
79
|
+
return payload or None
|
|
80
|
+
|
|
81
|
+
def create(self, json=None, targets=None, target_key=None, **fields):
|
|
82
|
+
payload = self._build_payload(
|
|
83
|
+
json=json, targets=targets, target_key=target_key, **fields
|
|
84
|
+
)
|
|
85
|
+
return ArcsecondAPIEndpoint.create(self, json=payload)
|
|
86
|
+
|
|
87
|
+
def update(self, id_name_uuid, json=None, targets=None, target_key=None, **fields):
|
|
88
|
+
payload = self._build_payload(
|
|
89
|
+
json=json, targets=targets, target_key=target_key, **fields
|
|
90
|
+
)
|
|
91
|
+
return ArcsecondAPIEndpoint.update(self, id_name_uuid, json=payload)
|
|
92
|
+
|
|
93
|
+
def upsert(self, match_field="name", json=None, targets=None, target_key=None, **fields):
|
|
94
|
+
payload = self._build_payload(
|
|
95
|
+
json=json, targets=targets, target_key=target_key, **fields
|
|
96
|
+
)
|
|
97
|
+
return super().upsert(match_field=match_field, json=payload)
|
|
98
|
+
|
|
99
|
+
def _read_target_refs(self, target_list, target_key=None):
|
|
100
|
+
key = target_key or self.target_relation_key
|
|
101
|
+
raw_targets = (target_list or {}).get(key, [])
|
|
102
|
+
refs = self._normalise_target_payloads(raw_targets) or []
|
|
103
|
+
return key, refs
|
|
104
|
+
|
|
105
|
+
def set_targets(self, id_name_uuid, targets, target_key=None):
|
|
106
|
+
target_key = target_key or self.target_relation_key
|
|
107
|
+
return self.update(id_name_uuid, **{target_key: self._normalise_target_payloads(targets)})
|
|
108
|
+
|
|
109
|
+
def clear_targets(self, id_name_uuid, target_key=None):
|
|
110
|
+
return self.set_targets(id_name_uuid, [], target_key=target_key)
|
|
111
|
+
|
|
112
|
+
def add_targets(self, id_name_uuid, targets, target_key=None):
|
|
113
|
+
target_list, error = self.read(id_name_uuid)
|
|
114
|
+
if error:
|
|
115
|
+
return None, error
|
|
116
|
+
|
|
117
|
+
key, current_refs = self._read_target_refs(target_list, target_key=target_key)
|
|
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
|
|
126
|
+
return self.update(id_name_uuid, **{key: current_refs})
|
|
127
|
+
|
|
128
|
+
def remove_targets(self, id_name_uuid, targets, target_key=None):
|
|
129
|
+
target_list, error = self.read(id_name_uuid)
|
|
130
|
+
if error:
|
|
131
|
+
return None, error
|
|
132
|
+
|
|
133
|
+
key, current_refs = self._read_target_refs(target_list, target_key=target_key)
|
|
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
|
+
]
|
|
143
|
+
return self.update(id_name_uuid, **{key: remaining_refs})
|
|
144
|
+
|
|
145
|
+
def add_target(self, id_name_uuid, target, target_key=None):
|
|
146
|
+
return self.add_targets(id_name_uuid, [target], target_key=target_key)
|
|
147
|
+
|
|
148
|
+
def remove_target(self, id_name_uuid, target, target_key=None):
|
|
149
|
+
return self.remove_targets(id_name_uuid, [target], target_key=target_key)
|
|
@@ -10,6 +10,7 @@ from arcsecond.cloud import (
|
|
|
10
10
|
upload_data,
|
|
11
11
|
)
|
|
12
12
|
from arcsecond.hosting import setup
|
|
13
|
+
from arcsecond.webcam import commands as webcam
|
|
13
14
|
|
|
14
15
|
from . import __version__
|
|
15
16
|
from .options import State
|
|
@@ -59,3 +60,6 @@ main.add_command(upload_data)
|
|
|
59
60
|
|
|
60
61
|
# Allow to try arcsecond by installing a local version
|
|
61
62
|
main.add_command(setup)
|
|
63
|
+
|
|
64
|
+
# Native webcam proxy — lets Docker containers reach USB webcams on the host.
|
|
65
|
+
main.add_command(webcam.webcam)
|
|
@@ -35,6 +35,12 @@ services:
|
|
|
35
35
|
depends_on:
|
|
36
36
|
- db
|
|
37
37
|
- broker
|
|
38
|
+
# Allows the backend to reach the host machine via host.docker.internal.
|
|
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
|
+
extra_hosts:
|
|
43
|
+
- "host.docker.internal:host-gateway"
|
|
38
44
|
env_file:
|
|
39
45
|
# You must have a .env file with secret keys beside this yml file.
|
|
40
46
|
- .env
|