arcsecond 3.17.0__tar.gz → 3.17.2__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.17.0 → arcsecond-3.17.2}/PKG-INFO +1 -1
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/utils.py +4 -2
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/commands.py +58 -12
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/registry.py +6 -1
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/sources/opencv.py +11 -8
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/store.py +29 -2
- {arcsecond-3.17.0 → arcsecond-3.17.2}/pyproject.toml +1 -1
- arcsecond-3.17.2/tests/test_hosting_utils_secret_key.py +10 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_imagesources_commands.py +73 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_imagesources_store.py +71 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_imagesources_webcam.py +16 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/uv.lock +1 -1
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.docker/Dockerfile_redis +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.flake8 +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.github/dependabot.yml +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.github/workflows/pythonpublish.yml +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.github/workflows/tests.yml +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/.gitignore +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/LICENSE +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/Makefile +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/README.md +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/__version__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/alpaca/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/alpaca/commands.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/alpaca/dome_probe.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/api/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/api/config.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/api/constants.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/api/endpoint.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/api/main.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/api/resources.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cli.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/auth.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/cloud/uploads.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/errors.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/backups.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/checks.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/constants.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/database.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/docker/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/docker/constants.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/docker/containers.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/docker/docker-compose.yml +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/docker/images.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/docker/utils.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/keygen/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/keygen/client.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/keygen/utils.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/local.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/main.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/postgres/init-db.sh +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/setup.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/hosting/validation.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/detection.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/proxy.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/runtime.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/sources/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/sources/base.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/sources/filewatch.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/imagesources/sources/network.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/options.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/arcsecond/targets.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/examples/example_upload_files.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/requirements.txt +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/setup.cfg +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/setup.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/sonar-project.properties +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/api/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/api/test_api.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/api/test_api_endpoint.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/api/test_config.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/api/test_targets.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/conftest.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/fixtures/file1.fits +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_alpaca_probe.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_cli.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_hosting_backups_restore.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_hosting_database.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_hosting_local.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_imagesources_detection.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_imagesources_network.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_imagesources_proxy.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/test_targets_planning.py +0 -0
- {arcsecond-3.17.0 → arcsecond-3.17.2}/tests/utils.py +0 -0
|
@@ -42,8 +42,10 @@ def _read_env_value(key, env_path=None):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def _get_random_secret_key():
|
|
45
|
-
#
|
|
46
|
-
|
|
45
|
+
# The value lands unquoted in .env, which Compose interpolates: '$name'
|
|
46
|
+
# would be swallowed (with a "variable is not set" warning) and '%' is
|
|
47
|
+
# unsafe in configparser-style readers. Neither belongs in the alphabet.
|
|
48
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#^&*(-_=+)"
|
|
47
49
|
return "".join(secrets.choice(chars) for _ in range(50))
|
|
48
50
|
|
|
49
51
|
|
|
@@ -40,6 +40,7 @@ import sys
|
|
|
40
40
|
import time
|
|
41
41
|
import urllib.request
|
|
42
42
|
from string import Template
|
|
43
|
+
from typing import Optional
|
|
43
44
|
from urllib.parse import urlsplit
|
|
44
45
|
|
|
45
46
|
import click
|
|
@@ -275,12 +276,25 @@ def _list_registered(kinds, noun: str, add_example: str):
|
|
|
275
276
|
|
|
276
277
|
click.echo(click.style(f"Registered {noun}s ({len(cameras)}):\n", bold=True))
|
|
277
278
|
_print_table(
|
|
278
|
-
[
|
|
279
|
-
|
|
279
|
+
[
|
|
280
|
+
(c.id, c.display_kind, c.target, _describe_specs(c), c.label or "")
|
|
281
|
+
for c in cameras
|
|
282
|
+
],
|
|
283
|
+
("ID", "KIND", "CAMERA", "DETAILS", "NAME"),
|
|
280
284
|
)
|
|
281
285
|
click.echo(f"\nForget one with: {_forget_example(kinds)} <ID>")
|
|
282
286
|
|
|
283
287
|
|
|
288
|
+
def _describe_specs(camera: Camera) -> str:
|
|
289
|
+
"""A camera's resolution and frame rate, as recorded when it was added."""
|
|
290
|
+
specs = camera.specs or {}
|
|
291
|
+
width, height, fps = specs.get("width"), specs.get("height"), specs.get("fps")
|
|
292
|
+
if not (width and height):
|
|
293
|
+
return ""
|
|
294
|
+
size = f"{width}×{height}"
|
|
295
|
+
return f"{size}, {fps:.0f} fps" if fps else size
|
|
296
|
+
|
|
297
|
+
|
|
284
298
|
def _detect_example(kinds) -> str:
|
|
285
299
|
return "arcsecond allsky detect" if ALLSKY in kinds else "arcsecond webcam detect"
|
|
286
300
|
|
|
@@ -506,9 +520,12 @@ def webcam_add_cmd(camera, label):
|
|
|
506
520
|
|
|
507
521
|
if target.isdigit():
|
|
508
522
|
index = int(target)
|
|
523
|
+
# One probe, used for both jobs: telling the operator if nothing is
|
|
524
|
+
# there, and recording the camera's resolution while it is open.
|
|
525
|
+
device = _probe_index(index)
|
|
509
526
|
_add(
|
|
510
|
-
Camera(id="", kind=USB, index=index, label=label),
|
|
511
|
-
absent_note=lambda:
|
|
527
|
+
Camera(id="", kind=USB, index=index, label=label, specs=_specs(device)),
|
|
528
|
+
absent_note=lambda: None if device else _nothing_at_index(index),
|
|
512
529
|
)
|
|
513
530
|
return
|
|
514
531
|
|
|
@@ -534,24 +551,53 @@ def webcam_add_cmd(camera, label):
|
|
|
534
551
|
_add(Camera(id="", kind=NET, url=target, label=label))
|
|
535
552
|
|
|
536
553
|
|
|
537
|
-
def
|
|
538
|
-
"""
|
|
554
|
+
def _probe_index(index: int):
|
|
555
|
+
"""The device at ``index`` right now, or None.
|
|
539
556
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
557
|
+
Returns None rather than raising when OpenCV is missing or the probe
|
|
558
|
+
fails: not knowing what is plugged in is a reason to say less, not a
|
|
559
|
+
reason to refuse to register a camera.
|
|
543
560
|
"""
|
|
544
561
|
try:
|
|
545
562
|
from .sources.opencv import detect_webcams
|
|
546
563
|
|
|
547
|
-
|
|
564
|
+
for device in detect_webcams():
|
|
565
|
+
if device.identity == ("usb", index):
|
|
566
|
+
return device
|
|
548
567
|
except Exception:
|
|
549
568
|
return None
|
|
550
|
-
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _specs(device) -> Optional[dict]:
|
|
573
|
+
"""Resolution and frame rate, as measured while the device was open.
|
|
574
|
+
|
|
575
|
+
Recorded so that listing a camera can report them without opening it —
|
|
576
|
+
`/detect` must stay answerable when a camera is busy or unplugged.
|
|
577
|
+
"""
|
|
578
|
+
if device is None:
|
|
551
579
|
return None
|
|
580
|
+
extra = device.extra or {}
|
|
581
|
+
specs = {
|
|
582
|
+
"width": extra.get("width"),
|
|
583
|
+
"height": extra.get("height"),
|
|
584
|
+
"fps": extra.get("fps"),
|
|
585
|
+
}
|
|
586
|
+
kept = {k: v for k, v in specs.items() if v}
|
|
587
|
+
return kept or None
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _nothing_at_index(index: int) -> str:
|
|
591
|
+
"""Said when a camera is registered while nothing is plugged in at its index.
|
|
592
|
+
|
|
593
|
+
It is registered either way: a webcam that is unplugged right now is still
|
|
594
|
+
a camera the operator owns, and refusing would mean re-typing the command
|
|
595
|
+
later. Saying nothing would let a typo sit unnoticed until the proxy failed
|
|
596
|
+
to open it.
|
|
597
|
+
"""
|
|
552
598
|
return (
|
|
553
599
|
f"nothing is at device index {index} right now. It stays registered — "
|
|
554
|
-
"plug it in
|
|
600
|
+
"plug it in, then re-run this command to record its resolution."
|
|
555
601
|
)
|
|
556
602
|
|
|
557
603
|
|
|
@@ -34,7 +34,12 @@ def build_source(camera: Camera) -> FrameSource:
|
|
|
34
34
|
``store.expanded``.
|
|
35
35
|
"""
|
|
36
36
|
if camera.kind == USB:
|
|
37
|
-
return OpenCVWebcamSource(
|
|
37
|
+
return OpenCVWebcamSource(
|
|
38
|
+
camera.index,
|
|
39
|
+
source_id=camera.id,
|
|
40
|
+
label=camera.label,
|
|
41
|
+
specs=camera.specs,
|
|
42
|
+
)
|
|
38
43
|
if camera.kind == NET:
|
|
39
44
|
return build_network_source(camera.id, camera.url, camera.label)
|
|
40
45
|
if camera.kind == ALLSKY:
|
|
@@ -51,8 +51,10 @@ class OpenCVWebcamSource(FrameSource):
|
|
|
51
51
|
index: int,
|
|
52
52
|
source_id: Optional[str] = None,
|
|
53
53
|
label: Optional[str] = None,
|
|
54
|
+
specs: Optional[dict] = None,
|
|
54
55
|
):
|
|
55
56
|
self.index = index
|
|
57
|
+
self.specs = specs or {}
|
|
56
58
|
# Registered cameras pass their own short id. The fallback only exists
|
|
57
59
|
# for probing a device that nobody has registered yet.
|
|
58
60
|
self.id = source_id or f"usb:{index}"
|
|
@@ -100,14 +102,15 @@ class OpenCVWebcamSource(FrameSource):
|
|
|
100
102
|
self._cap = None
|
|
101
103
|
|
|
102
104
|
def info(self) -> SourceInfo:
|
|
103
|
-
# Width
|
|
104
|
-
# open anything —
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
# Width, height and fps are only knowable by opening the device, and
|
|
106
|
+
# listing must not open anything — one camera being busy would hold the
|
|
107
|
+
# answer up for every other. They are measured once, when the camera is
|
|
108
|
+
# registered, and reported from the store here.
|
|
109
|
+
extra = {"transport": "usb", "index": self.index}
|
|
110
|
+
for key in ("width", "height", "fps"):
|
|
111
|
+
if self.specs.get(key):
|
|
112
|
+
extra[key] = self.specs[key]
|
|
113
|
+
return SourceInfo(id=self.id, kind=self.kind, label=self.label, extra=extra)
|
|
111
114
|
|
|
112
115
|
|
|
113
116
|
def detect_webcams(max_index: int = _MAX_PROBE) -> list[DetectedDevice]:
|
|
@@ -91,6 +91,11 @@ class Camera:
|
|
|
91
91
|
index: Optional[int] = None # usb
|
|
92
92
|
url: Optional[str] = None # net
|
|
93
93
|
path: Optional[str] = None # allsky
|
|
94
|
+
# What probing learned about a USB camera: width, height, fps. Recorded at
|
|
95
|
+
# registration, when the device is opened anyway, so that listing it later
|
|
96
|
+
# can report them without opening anything. Absent for a camera registered
|
|
97
|
+
# while unplugged, and for the kinds that have no such thing.
|
|
98
|
+
specs: Optional[dict] = None
|
|
94
99
|
|
|
95
100
|
@property
|
|
96
101
|
def identity(self) -> tuple:
|
|
@@ -134,6 +139,8 @@ class Camera:
|
|
|
134
139
|
entry: dict = {"kind": self.kind}
|
|
135
140
|
if self.kind == USB:
|
|
136
141
|
entry["index"] = self.index
|
|
142
|
+
if self.specs:
|
|
143
|
+
entry["specs"] = self.specs
|
|
137
144
|
elif self.kind == NET:
|
|
138
145
|
entry["url"] = self.url
|
|
139
146
|
else:
|
|
@@ -154,7 +161,14 @@ def camera_from_json(cam_id: str, entry: dict) -> Optional[Camera]:
|
|
|
154
161
|
index = entry.get("index")
|
|
155
162
|
if not isinstance(index, int):
|
|
156
163
|
return None
|
|
157
|
-
|
|
164
|
+
specs = entry.get("specs")
|
|
165
|
+
return Camera(
|
|
166
|
+
id=cam_id,
|
|
167
|
+
kind=USB,
|
|
168
|
+
index=index,
|
|
169
|
+
label=label,
|
|
170
|
+
specs=specs if isinstance(specs, dict) else None,
|
|
171
|
+
)
|
|
158
172
|
if kind == NET:
|
|
159
173
|
url = entry.get("url")
|
|
160
174
|
if not url:
|
|
@@ -345,8 +359,17 @@ def add(camera: Camera, path: Optional[Path] = None) -> tuple[Camera, bool]:
|
|
|
345
359
|
|
|
346
360
|
for existing in stored.values():
|
|
347
361
|
if existing.identity == camera.identity:
|
|
362
|
+
# Re-registering the same camera is how its label and its measured
|
|
363
|
+
# resolution get refreshed — a camera that was unplugged the first
|
|
364
|
+
# time, or that has been set to a different mode since.
|
|
365
|
+
changed = False
|
|
348
366
|
if camera.label and camera.label != existing.label:
|
|
349
367
|
existing.label = camera.label
|
|
368
|
+
changed = True
|
|
369
|
+
if camera.specs and camera.specs != existing.specs:
|
|
370
|
+
existing.specs = camera.specs
|
|
371
|
+
changed = True
|
|
372
|
+
if changed:
|
|
350
373
|
data["cameras"][existing.id] = existing.to_json()
|
|
351
374
|
save(data, path)
|
|
352
375
|
return existing, False
|
|
@@ -403,7 +426,11 @@ def expanded(cameras: list[Camera], expand) -> list[Camera]:
|
|
|
403
426
|
continue
|
|
404
427
|
usable.append(
|
|
405
428
|
Camera(
|
|
406
|
-
id=camera.id,
|
|
429
|
+
id=camera.id,
|
|
430
|
+
kind=NET,
|
|
431
|
+
url=url,
|
|
432
|
+
label=camera.label,
|
|
433
|
+
path=camera.path,
|
|
407
434
|
)
|
|
408
435
|
)
|
|
409
436
|
return usable
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from arcsecond.hosting.utils import _get_random_secret_key
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_secret_key_has_no_compose_interpolation_characters():
|
|
5
|
+
# Compose interpolates '$name' inside .env; '%' trips configparser readers.
|
|
6
|
+
for _ in range(200):
|
|
7
|
+
key = _get_random_secret_key()
|
|
8
|
+
assert len(key) == 50
|
|
9
|
+
assert "$" not in key
|
|
10
|
+
assert "%" not in key
|
|
@@ -641,3 +641,76 @@ def test_a_proxy_is_only_stopped_once_its_process_is_gone(monkeypatch):
|
|
|
641
641
|
assert commands._has_exited(8765, 4711) is False
|
|
642
642
|
alive["gone"] = True
|
|
643
643
|
assert commands._has_exited(8765, 4711) is True
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# ---------------------------------------------------------------------------
|
|
647
|
+
# Resolution and frame rate
|
|
648
|
+
# ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _device(index=0, width=1280, height=720, fps=30.0):
|
|
652
|
+
return DetectedDevice(
|
|
653
|
+
kind="usb",
|
|
654
|
+
identity=("usb", index),
|
|
655
|
+
label=f"USB webcam #{index}",
|
|
656
|
+
extra={"index": index, "width": width, "height": height, "fps": fps},
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def test_add_records_what_it_measured_while_the_device_was_open(cli, monkeypatch):
|
|
661
|
+
monkeypatch.setattr(
|
|
662
|
+
"arcsecond.imagesources.sources.opencv.detect_webcams",
|
|
663
|
+
lambda *a, **k: [_device()],
|
|
664
|
+
)
|
|
665
|
+
_run(cli, webcam, ["add", "0"])
|
|
666
|
+
(camera,) = store.all_cameras()
|
|
667
|
+
assert camera.specs == {"width": 1280, "height": 720, "fps": 30.0}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def test_listing_shows_the_resolution_without_probing(cli, monkeypatch):
|
|
671
|
+
monkeypatch.setattr(
|
|
672
|
+
"arcsecond.imagesources.sources.opencv.detect_webcams",
|
|
673
|
+
lambda *a, **k: [_device()],
|
|
674
|
+
)
|
|
675
|
+
_run(cli, webcam, ["add", "0"])
|
|
676
|
+
|
|
677
|
+
def explode(*a, **k):
|
|
678
|
+
raise AssertionError("listing probed the hardware")
|
|
679
|
+
|
|
680
|
+
monkeypatch.setattr("arcsecond.imagesources.sources.opencv.detect_webcams", explode)
|
|
681
|
+
assert "1280×720, 30 fps" in _run(cli, webcam, []).output
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def test_a_camera_added_while_unplugged_says_how_to_fill_them_in(cli, monkeypatch):
|
|
685
|
+
_no_webcams(monkeypatch)
|
|
686
|
+
result = _run(cli, webcam, ["add", "0"])
|
|
687
|
+
assert "re-run this command to record its resolution" in result.output
|
|
688
|
+
assert store.all_cameras()[0].specs is None
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def test_re_adding_once_plugged_in_records_the_resolution(cli, monkeypatch):
|
|
692
|
+
_no_webcams(monkeypatch)
|
|
693
|
+
_run(cli, webcam, ["add", "0"])
|
|
694
|
+
monkeypatch.setattr(
|
|
695
|
+
"arcsecond.imagesources.sources.opencv.detect_webcams",
|
|
696
|
+
lambda *a, **k: [_device()],
|
|
697
|
+
)
|
|
698
|
+
result = _run(cli, webcam, ["add", "0"])
|
|
699
|
+
assert "Already registered" in result.output
|
|
700
|
+
assert store.all_cameras()[0].specs["width"] == 1280
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def test_the_proxy_is_given_the_recorded_resolution(cli, monkeypatch):
|
|
704
|
+
"""It is what /detect reports, and what the backend shows the operator."""
|
|
705
|
+
monkeypatch.setattr(
|
|
706
|
+
"arcsecond.imagesources.sources.opencv.detect_webcams",
|
|
707
|
+
lambda *a, **k: [_device()],
|
|
708
|
+
)
|
|
709
|
+
_run(cli, webcam, ["add", "0"])
|
|
710
|
+
|
|
711
|
+
started = {}
|
|
712
|
+
monkeypatch.setattr(
|
|
713
|
+
"arcsecond.imagesources.proxy.run", lambda **k: started.update(k)
|
|
714
|
+
)
|
|
715
|
+
_run(cli, proxy, ["start", "--foreground"])
|
|
716
|
+
assert started["cameras"][0].specs == {"width": 1280, "height": 720, "fps": 30.0}
|
|
@@ -296,3 +296,74 @@ def test_migration_does_not_expand_the_password(store_file):
|
|
|
296
296
|
store_file.write_text(json.dumps(V1))
|
|
297
297
|
store.all_cameras(store_file)
|
|
298
298
|
assert "${DOME_CAM_PW}" in store_file.read_text()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# What probing learned about a USB camera
|
|
303
|
+
#
|
|
304
|
+
# Resolution and frame rate can only be measured by opening the device, and
|
|
305
|
+
# `/detect` must stay answerable while a camera is busy or unplugged. So they
|
|
306
|
+
# are measured once, at registration, and read back from the store afterwards.
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
SPECS = {"width": 1280, "height": 720, "fps": 30.0}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_a_usb_camera_remembers_what_probing_measured(store_file):
|
|
314
|
+
stored, _ = store.add(Camera(id="", kind=USB, index=0, specs=SPECS), store_file)
|
|
315
|
+
assert store.find(stored.id, store_file).specs == SPECS
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_specs_survive_the_round_trip_to_disk(store_file):
|
|
319
|
+
store.add(Camera(id="", kind=USB, index=0, specs=SPECS), store_file)
|
|
320
|
+
written = json.loads(store_file.read_text())
|
|
321
|
+
assert written["cameras"][store.all_cameras(store_file)[0].id]["specs"] == SPECS
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_a_camera_registered_while_unplugged_simply_has_none(store_file):
|
|
325
|
+
stored, _ = store.add(_usb(0), store_file)
|
|
326
|
+
assert store.find(stored.id, store_file).specs is None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_re_registering_records_specs_measured_the_second_time(store_file):
|
|
330
|
+
"""Plugging the camera in and re-running `add` is how they get filled in."""
|
|
331
|
+
first, _ = store.add(_usb(0), store_file)
|
|
332
|
+
second, created = store.add(
|
|
333
|
+
Camera(id="", kind=USB, index=0, specs=SPECS), store_file
|
|
334
|
+
)
|
|
335
|
+
assert created is False
|
|
336
|
+
assert second.id == first.id
|
|
337
|
+
assert store.find(first.id, store_file).specs == SPECS
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_re_registering_without_specs_does_not_wipe_the_recorded_ones(store_file):
|
|
341
|
+
"""A re-run on a machine without OpenCV must not lose what was measured."""
|
|
342
|
+
stored, _ = store.add(Camera(id="", kind=USB, index=0, specs=SPECS), store_file)
|
|
343
|
+
store.add(_usb(0), store_file)
|
|
344
|
+
assert store.find(stored.id, store_file).specs == SPECS
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_specs_are_not_part_of_what_identifies_a_camera(store_file):
|
|
348
|
+
"""A camera switched to another mode is the same camera, not a new one."""
|
|
349
|
+
first, _ = store.add(Camera(id="", kind=USB, index=0, specs=SPECS), store_file)
|
|
350
|
+
other = dict(SPECS, width=640, height=480)
|
|
351
|
+
second, created = store.add(
|
|
352
|
+
Camera(id="", kind=USB, index=0, specs=other), store_file
|
|
353
|
+
)
|
|
354
|
+
assert (created, second.id) == (False, first.id)
|
|
355
|
+
assert len(store.all_cameras(store_file)) == 1
|
|
356
|
+
assert store.find(first.id, store_file).specs == other
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_a_malformed_specs_entry_is_ignored_rather_than_fatal(store_file):
|
|
360
|
+
store_file.write_text(
|
|
361
|
+
json.dumps(
|
|
362
|
+
{
|
|
363
|
+
"version": 2,
|
|
364
|
+
"cameras": {"aaa": {"kind": "usb", "index": 0, "specs": "1280x720"}},
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
(camera,) = store.all_cameras(store_file)
|
|
369
|
+
assert camera.specs is None
|
|
@@ -154,3 +154,19 @@ def test_open_raises_when_the_device_will_not_open():
|
|
|
154
154
|
assert "device index 0" in str(e)
|
|
155
155
|
else:
|
|
156
156
|
raise AssertionError("expected open() to raise")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_info_reports_the_recorded_resolution_without_opening_anything():
|
|
160
|
+
source = OpenCVWebcamSource(
|
|
161
|
+
0, source_id="k3f", specs={"width": 1280, "height": 720, "fps": 30.0}
|
|
162
|
+
)
|
|
163
|
+
# No cv2 in sys.modules for this call: reporting must not need the device.
|
|
164
|
+
extra = source.info().extra
|
|
165
|
+
assert (extra["width"], extra["height"], extra["fps"]) == (1280, 720, 30.0)
|
|
166
|
+
assert extra["transport"] == "usb"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_info_omits_what_was_never_measured():
|
|
170
|
+
extra = OpenCVWebcamSource(0, source_id="k3f").info().extra
|
|
171
|
+
assert "width" not in extra and "fps" not in extra
|
|
172
|
+
assert extra == {"transport": "usb", "index": 0}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_errors.py
RENAMED
|
File without changes
|
{arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py
RENAMED
|
File without changes
|
|
File without changes
|
{arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_prepare.py
RENAMED
|
File without changes
|
{arcsecond-3.17.0 → arcsecond-3.17.2}/tests/cloud/uploader/datafiles/test_uploader_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|