arcsecond 3.18.0__tar.gz → 3.18.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.18.0 → arcsecond-3.18.2}/PKG-INFO +1 -1
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/commands.py +40 -3
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/detection.py +13 -6
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/sources/filewatch.py +13 -2
- {arcsecond-3.18.0 → arcsecond-3.18.2}/pyproject.toml +1 -1
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_imagesources_commands.py +74 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_imagesources_detection.py +56 -8
- {arcsecond-3.18.0 → arcsecond-3.18.2}/uv.lock +1 -1
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.docker/Dockerfile_redis +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.flake8 +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.github/dependabot.yml +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.github/workflows/pythonpublish.yml +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.github/workflows/tests.yml +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/.gitignore +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/LICENSE +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/Makefile +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/README.md +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/__version__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/alpaca/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/alpaca/commands.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/alpaca/dome_probe.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/api/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/api/config.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/api/constants.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/api/endpoint.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/api/main.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/api/resources.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cli.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/auth.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/cloud/uploads.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/errors.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/backups.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/checks.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/constants.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/database.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/docker/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/docker/constants.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/docker/containers.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/docker/docker-compose.yml +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/docker/images.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/docker/utils.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/keygen/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/keygen/client.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/keygen/utils.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/local.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/main.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/postgres/init-db.sh +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/setup.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/utils.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/hosting/validation.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/proxy.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/registry.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/runtime.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/sources/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/sources/base.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/sources/network.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/sources/opencv.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/imagesources/store.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/options.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/arcsecond/targets.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/examples/example_upload_files.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/requirements.txt +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/setup.cfg +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/setup.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/sonar-project.properties +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/api/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/api/test_api.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/api/test_api_endpoint.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/api/test_config.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/api/test_targets.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/conftest.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/fixtures/file1.fits +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_alpaca_probe.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_cli.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_hosting_backups_restore.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_hosting_database.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_hosting_local.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_hosting_utils_secret_key.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_imagesources_network.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_imagesources_proxy.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_imagesources_store.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_imagesources_webcam.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/test_targets_planning.py +0 -0
- {arcsecond-3.18.0 → arcsecond-3.18.2}/tests/utils.py +0 -0
|
@@ -551,7 +551,10 @@ def webcam_add_cmd(camera, label):
|
|
|
551
551
|
+ ".",
|
|
552
552
|
)
|
|
553
553
|
|
|
554
|
-
_add(
|
|
554
|
+
_add(
|
|
555
|
+
Camera(id="", kind=NET, url=target, label=label),
|
|
556
|
+
absent_note=lambda: _unreachable_note(expanded),
|
|
557
|
+
)
|
|
555
558
|
|
|
556
559
|
|
|
557
560
|
def _probe_index(index: int):
|
|
@@ -590,6 +593,37 @@ def _specs(device) -> Optional[dict]:
|
|
|
590
593
|
return kept or None
|
|
591
594
|
|
|
592
595
|
|
|
596
|
+
def _unreachable_note(url: str) -> Optional[str]:
|
|
597
|
+
"""Said when a camera is registered at an address that does not answer.
|
|
598
|
+
|
|
599
|
+
It is registered either way, for the same reason an unplugged webcam is: a
|
|
600
|
+
camera that is switched off, or on a machine still booting, is one the
|
|
601
|
+
operator owns. But saying nothing lets a typo — or a name this machine
|
|
602
|
+
cannot resolve — surface hours later and two machines away, as a camera
|
|
603
|
+
that is simply blank in Arcsecond.local, instead of as an answer to the
|
|
604
|
+
line just typed.
|
|
605
|
+
"""
|
|
606
|
+
reachable, why = detection.is_reachable(url)
|
|
607
|
+
if reachable:
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
note = (
|
|
611
|
+
f"{why}. It stays registered — the proxy will serve it as soon as it "
|
|
612
|
+
"answers."
|
|
613
|
+
)
|
|
614
|
+
host = (urlsplit(url).hostname or "").lower()
|
|
615
|
+
if host.endswith(".local"):
|
|
616
|
+
# The failure that is worth explaining rather than merely reporting:
|
|
617
|
+
# the browser you found the address in resolves .local names over
|
|
618
|
+
# mDNS, and the proxy asks the machine's resolver, which often cannot.
|
|
619
|
+
note += (
|
|
620
|
+
" A .local name is answered by that machine itself over mDNS, "
|
|
621
|
+
"which not every computer can do — registering its IP address "
|
|
622
|
+
"instead is the reliable form."
|
|
623
|
+
)
|
|
624
|
+
return note
|
|
625
|
+
|
|
626
|
+
|
|
593
627
|
def _nothing_at_index(index: int) -> str:
|
|
594
628
|
"""Said when a camera is registered while nothing is plugged in at its index.
|
|
595
629
|
|
|
@@ -835,8 +869,11 @@ def allsky_add_cmd(target, label):
|
|
|
835
869
|
# password stays a variable name on disk. Doing it here is what makes an
|
|
836
870
|
# unset variable a complaint about the line just typed, rather than a
|
|
837
871
|
# camera quietly missing from the next `proxy start`.
|
|
838
|
-
_expand_env_vars(target)
|
|
839
|
-
_add(
|
|
872
|
+
expanded = _expand_env_vars(target)
|
|
873
|
+
_add(
|
|
874
|
+
Camera(id="", kind=ALLSKY, url=target, label=label),
|
|
875
|
+
absent_note=lambda: _unreachable_note(expanded),
|
|
876
|
+
)
|
|
840
877
|
return
|
|
841
878
|
|
|
842
879
|
# A single letter is a Windows drive, not a scheme: `C:\allsky\latest.jpg`
|
|
@@ -96,20 +96,27 @@ def _network_endpoint(url: str) -> Optional[tuple]:
|
|
|
96
96
|
return (host, port)
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def
|
|
100
|
-
"""``(reachable, why)`` for one camera
|
|
99
|
+
def is_reachable(url: str, timeout: float = NETWORK_TIMEOUT) -> tuple:
|
|
100
|
+
"""``(reachable, why)`` for one camera address.
|
|
101
|
+
|
|
102
|
+
Takes the URL rather than the camera because ``add`` asks this about an
|
|
103
|
+
address that is not registered yet, and ``detect`` about one that is.
|
|
101
104
|
|
|
102
105
|
Only the address is contacted, and the answer never contains the URL's
|
|
103
|
-
password:
|
|
104
|
-
the host and port alone.
|
|
106
|
+
password: the endpoint named here is the host and port alone.
|
|
105
107
|
"""
|
|
106
|
-
endpoint = _network_endpoint(
|
|
108
|
+
endpoint = _network_endpoint(url or "")
|
|
107
109
|
if endpoint is None:
|
|
108
110
|
return False, "the address cannot be read"
|
|
109
111
|
host, port = endpoint
|
|
110
112
|
try:
|
|
111
113
|
with socket.create_connection((host, port), timeout=timeout):
|
|
112
114
|
return True, f"answering on {host}:{port}"
|
|
115
|
+
except socket.gaierror:
|
|
116
|
+
# Told apart from a connection that fails: nothing is wrong with the
|
|
117
|
+
# camera, the name simply does not resolve on this machine, and that
|
|
118
|
+
# is a different thing to go and fix.
|
|
119
|
+
return False, f"{host} cannot be resolved"
|
|
113
120
|
except OSError as e:
|
|
114
121
|
return False, f"no answer on {host}:{port} ({e.strerror or e})"
|
|
115
122
|
|
|
@@ -152,7 +159,7 @@ def _reachability(cameras: list[Camera], timeout: float) -> dict:
|
|
|
152
159
|
if not cameras:
|
|
153
160
|
return {}
|
|
154
161
|
with ThreadPoolExecutor(max_workers=min(8, len(cameras))) as pool:
|
|
155
|
-
outcomes = pool.map(lambda c:
|
|
162
|
+
outcomes = pool.map(lambda c: is_reachable(c.url or "", timeout), cameras)
|
|
156
163
|
return {camera.id: outcome for camera, outcome in zip(cameras, outcomes)}
|
|
157
164
|
|
|
158
165
|
|
|
@@ -21,10 +21,21 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
_POLL_INTERVAL = 5.0 # seconds — image cadence is much slower; this is fine
|
|
22
22
|
|
|
23
23
|
# Common locations to probe at startup. First match wins per software.
|
|
24
|
+
#
|
|
25
|
+
# For Thomas Jacquin's allsky these are its own variables, resolved: since the
|
|
26
|
+
# 2023 rearrangement everything lives under ALLSKY_HOME (``~/allsky``), with
|
|
27
|
+
# ALLSKY_TMP at ``${ALLSKY_HOME}/tmp`` and the local website at
|
|
28
|
+
# ``${ALLSKY_HOME}/html/allsky``. Installs older than that kept the website in
|
|
29
|
+
# ``/var/www/html/allsky`` instead. Beware the ``/current/tmp/`` in the URL its
|
|
30
|
+
# web interface uses: that is a lighttpd alias for ALLSKY_TMP, not a directory,
|
|
31
|
+
# and pasting it in here as a path is how this list went stale before.
|
|
24
32
|
ALLSKY_DISCOVERY_PATHS: list[Path] = [
|
|
25
|
-
# Thomas Jacquin's allsky (https://github.com/AllskyTeam/allsky)
|
|
33
|
+
# Thomas Jacquin's allsky (https://github.com/AllskyTeam/allsky).
|
|
34
|
+
# ALLSKY_TMP first: the capture loop rewrites it every cycle, and it is
|
|
35
|
+
# there whether or not the local website is enabled.
|
|
26
36
|
Path.home() / "allsky" / "tmp" / "image.jpg",
|
|
27
|
-
Path("/
|
|
37
|
+
Path.home() / "allsky" / "html" / "allsky" / "image.jpg",
|
|
38
|
+
Path("/var/www/html/allsky/image.jpg"),
|
|
28
39
|
# indi-allsky (https://github.com/aaronwmorris/indi-allsky)
|
|
29
40
|
Path("/var/lib/indi-allsky/images/latest.jpg"),
|
|
30
41
|
Path.home() / "indi-allsky" / "latest.jpg",
|
|
@@ -39,6 +39,15 @@ def cli(tmp_path, monkeypatch):
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
monkeypatch.setattr(commands.subprocess, "Popen", no_real_processes)
|
|
42
|
+
|
|
43
|
+
# `add` checks that an address answers before saying it is registered.
|
|
44
|
+
# Left alone, every test registering a camera would open a real socket to
|
|
45
|
+
# a made-up host and wait out the timeout — 2 seconds each, and a suite
|
|
46
|
+
# whose result depends on what the network does. Reachable is the quiet
|
|
47
|
+
# default; the tests about the note say otherwise for themselves.
|
|
48
|
+
monkeypatch.setattr(
|
|
49
|
+
commands.detection, "is_reachable", lambda url, **k: (True, "answering")
|
|
50
|
+
)
|
|
42
51
|
return CliRunner()
|
|
43
52
|
|
|
44
53
|
|
|
@@ -230,6 +239,71 @@ def test_an_address_allsky_cannot_read_is_refused(cli):
|
|
|
230
239
|
assert store.all_cameras() == []
|
|
231
240
|
|
|
232
241
|
|
|
242
|
+
def test_an_address_that_does_not_answer_is_registered_with_a_note(cli, monkeypatch):
|
|
243
|
+
"""A camera that is off is still a camera — but the operator is told now."""
|
|
244
|
+
monkeypatch.setattr(
|
|
245
|
+
commands.detection,
|
|
246
|
+
"is_reachable",
|
|
247
|
+
lambda url, **k: (False, "no answer on 10.0.0.9:80 (Connection refused)"),
|
|
248
|
+
)
|
|
249
|
+
result = _run(cli, allsky, ["add", "http://10.0.0.9/latest.jpg"])
|
|
250
|
+
assert result.exit_code == 0
|
|
251
|
+
assert "Note:" in result.output
|
|
252
|
+
assert "no answer on 10.0.0.9:80" in result.output
|
|
253
|
+
assert len(store.all_cameras()) == 1
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_an_address_that_answers_says_nothing_extra(cli):
|
|
257
|
+
result = _run(cli, allsky, ["add", "http://10.0.0.9/latest.jpg"])
|
|
258
|
+
assert "Note:" not in result.output
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_a_local_name_that_will_not_resolve_explains_itself(cli, monkeypatch):
|
|
262
|
+
"""The failure worth explaining: the browser resolves .local, we may not."""
|
|
263
|
+
monkeypatch.setattr(
|
|
264
|
+
commands.detection,
|
|
265
|
+
"is_reachable",
|
|
266
|
+
lambda url, **k: (False, "skykot.local cannot be resolved"),
|
|
267
|
+
)
|
|
268
|
+
result = _run(cli, allsky, ["add", "http://skykot.local/current/tmp/image.jpg"])
|
|
269
|
+
assert "mDNS" in result.output
|
|
270
|
+
assert "IP address" in result.output
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_a_network_camera_that_does_not_answer_is_noted_too(cli, monkeypatch):
|
|
274
|
+
monkeypatch.setattr(
|
|
275
|
+
commands.detection, "is_reachable", lambda url, **k: (False, "no answer")
|
|
276
|
+
)
|
|
277
|
+
result = _run(cli, webcam, ["add", "rtsp://cam.local/s"])
|
|
278
|
+
assert result.exit_code == 0
|
|
279
|
+
assert "Note:" in result.output
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_the_address_contacted_is_the_one_with_the_password_filled_in(cli, monkeypatch):
|
|
283
|
+
"""The check must reach the real camera, not `${VARIABLE}` as a hostname."""
|
|
284
|
+
monkeypatch.setenv("PW", "hunter2")
|
|
285
|
+
asked = []
|
|
286
|
+
monkeypatch.setattr(
|
|
287
|
+
commands.detection,
|
|
288
|
+
"is_reachable",
|
|
289
|
+
lambda url, **k: (asked.append(url), (True, "answering"))[1],
|
|
290
|
+
)
|
|
291
|
+
_run(cli, allsky, ["add", "http://sky:${PW}@10.0.0.9/latest.jpg"])
|
|
292
|
+
assert asked == ["http://sky:hunter2@10.0.0.9/latest.jpg"]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_re_running_an_add_does_not_contact_the_camera_again(cli, monkeypatch):
|
|
296
|
+
"""Re-running a line from history should be quiet, and cost nothing."""
|
|
297
|
+
_run(cli, allsky, ["add", "http://10.0.0.9/latest.jpg"])
|
|
298
|
+
monkeypatch.setattr(
|
|
299
|
+
commands.detection,
|
|
300
|
+
"is_reachable",
|
|
301
|
+
lambda url, **k: (_ for _ in ()).throw(AssertionError("contacted again")),
|
|
302
|
+
)
|
|
303
|
+
result = _run(cli, allsky, ["add", "http://10.0.0.9/latest.jpg"])
|
|
304
|
+
assert "Already registered" in result.output
|
|
305
|
+
|
|
306
|
+
|
|
233
307
|
def test_a_password_in_an_all_sky_address_is_never_written_in_the_clear(
|
|
234
308
|
cli, monkeypatch, tmp_path
|
|
235
309
|
):
|
|
@@ -106,7 +106,7 @@ def test_all_sky_detection_ignores_webcams():
|
|
|
106
106
|
|
|
107
107
|
def test_a_reachable_network_camera_is_present():
|
|
108
108
|
camera = Camera(id="abc", kind=NET, url="rtsp://cam.local/s")
|
|
109
|
-
with patch.object(detection, "
|
|
109
|
+
with patch.object(detection, "is_reachable", return_value=(True, "answering")):
|
|
110
110
|
report = _report([camera])
|
|
111
111
|
assert report.present == [camera]
|
|
112
112
|
assert report.new == []
|
|
@@ -114,7 +114,7 @@ def test_a_reachable_network_camera_is_present():
|
|
|
114
114
|
|
|
115
115
|
def test_an_unreachable_network_camera_is_missing_with_a_reason():
|
|
116
116
|
camera = Camera(id="abc", kind=NET, url="rtsp://cam.local/s")
|
|
117
|
-
with patch.object(detection, "
|
|
117
|
+
with patch.object(detection, "is_reachable", return_value=(False, "no answer")):
|
|
118
118
|
report = _report([camera])
|
|
119
119
|
assert report.missing == [camera]
|
|
120
120
|
assert report.detail["abc"] == "no answer"
|
|
@@ -122,19 +122,38 @@ def test_an_unreachable_network_camera_is_missing_with_a_reason():
|
|
|
122
122
|
|
|
123
123
|
def test_no_network_says_so_rather_than_guessing():
|
|
124
124
|
camera = Camera(id="abc", kind=NET, url="rtsp://cam.local/s")
|
|
125
|
-
with patch.object(detection, "
|
|
125
|
+
with patch.object(detection, "is_reachable", side_effect=AssertionError("called")):
|
|
126
126
|
report = _report([camera], check_network=False)
|
|
127
127
|
assert report.present == [camera]
|
|
128
128
|
assert report.detail["abc"] == "not contacted"
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
def test_reachability_never_leaks_the_password():
|
|
132
|
-
|
|
133
|
-
reachable, why = detection.
|
|
132
|
+
url = "rtsp://admin:hunter2@127.0.0.1:1/s"
|
|
133
|
+
reachable, why = detection.is_reachable(url, timeout=0.2)
|
|
134
134
|
assert reachable is False
|
|
135
135
|
assert "hunter2" not in why
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def test_a_name_that_does_not_resolve_says_so(monkeypatch):
|
|
139
|
+
"""Distinct from a refused connection: the camera may be perfectly fine."""
|
|
140
|
+
import socket as socket_module
|
|
141
|
+
|
|
142
|
+
def no_such_name(*a, **k):
|
|
143
|
+
raise socket_module.gaierror(-2, "Name or service not known")
|
|
144
|
+
|
|
145
|
+
monkeypatch.setattr(detection.socket, "create_connection", no_such_name)
|
|
146
|
+
reachable, why = detection.is_reachable("http://skykot.local/image.jpg")
|
|
147
|
+
assert reachable is False
|
|
148
|
+
assert why == "skykot.local cannot be resolved"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_an_address_that_cannot_be_read_is_not_contacted():
|
|
152
|
+
reachable, why = detection.is_reachable("http://")
|
|
153
|
+
assert reachable is False
|
|
154
|
+
assert why == "the address cannot be read"
|
|
155
|
+
|
|
156
|
+
|
|
138
157
|
@pytest.mark.parametrize(
|
|
139
158
|
"url,expected",
|
|
140
159
|
[
|
|
@@ -201,7 +220,7 @@ def test_a_detected_all_sky_camera_that_is_registered_is_not_reported_twice(tmp_
|
|
|
201
220
|
def test_an_all_sky_camera_at_an_address_is_confirmed_by_contacting_it():
|
|
202
221
|
"""It has no path to look at, so it is settled the way any address is."""
|
|
203
222
|
camera = Camera(id="abc", kind=ALLSKY, url="http://sky.local/latest.jpg")
|
|
204
|
-
with patch.object(detection, "
|
|
223
|
+
with patch.object(detection, "is_reachable", return_value=(True, "answering")):
|
|
205
224
|
with patch.object(detection, "detect_allsky", return_value=[]):
|
|
206
225
|
report = detection.report([camera], (ALLSKY,))
|
|
207
226
|
assert report.present == [camera]
|
|
@@ -210,7 +229,7 @@ def test_an_all_sky_camera_at_an_address_is_confirmed_by_contacting_it():
|
|
|
210
229
|
|
|
211
230
|
def test_an_all_sky_camera_at_an_address_that_is_down_is_missing():
|
|
212
231
|
camera = Camera(id="abc", kind=ALLSKY, url="http://sky.local/latest.jpg")
|
|
213
|
-
with patch.object(detection, "
|
|
232
|
+
with patch.object(detection, "is_reachable", return_value=(False, "no answer")):
|
|
214
233
|
with patch.object(detection, "detect_allsky", return_value=[]):
|
|
215
234
|
report = detection.report([camera], (ALLSKY,))
|
|
216
235
|
assert report.missing == [camera]
|
|
@@ -221,12 +240,41 @@ def test_an_all_sky_address_is_never_looked_for_on_this_disk():
|
|
|
221
240
|
"""The old check called a URL a missing file — the wrong answer entirely."""
|
|
222
241
|
camera = Camera(id="abc", kind=ALLSKY, url="http://sky.local/latest.jpg")
|
|
223
242
|
with patch.object(detection, "resolve_allsky_path") as looked_at_disk:
|
|
224
|
-
with patch.object(detection, "
|
|
243
|
+
with patch.object(detection, "is_reachable", return_value=(True, "answering")):
|
|
225
244
|
with patch.object(detection, "detect_allsky", return_value=[]):
|
|
226
245
|
detection.report([camera], (ALLSKY,))
|
|
227
246
|
looked_at_disk.assert_not_called()
|
|
228
247
|
|
|
229
248
|
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# Probing the well-known locations
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_an_image_at_a_well_known_location_is_detected(tmp_path, monkeypatch):
|
|
255
|
+
from arcsecond.imagesources.sources import filewatch
|
|
256
|
+
|
|
257
|
+
image = tmp_path / "image.jpg"
|
|
258
|
+
image.write_bytes(b"x")
|
|
259
|
+
monkeypatch.setattr(filewatch, "ALLSKY_DISCOVERY_PATHS", [image])
|
|
260
|
+
|
|
261
|
+
(device,) = filewatch.detect_allsky()
|
|
262
|
+
assert device.identity == (ALLSKY, str(image))
|
|
263
|
+
assert device.extra["path"] == str(image)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_a_well_known_location_with_nothing_at_it_is_not_reported(
|
|
267
|
+
tmp_path, monkeypatch
|
|
268
|
+
):
|
|
269
|
+
"""Probing reports what is there — an empty list is an answer, not a fault."""
|
|
270
|
+
from arcsecond.imagesources.sources import filewatch
|
|
271
|
+
|
|
272
|
+
monkeypatch.setattr(
|
|
273
|
+
filewatch, "ALLSKY_DISCOVERY_PATHS", [tmp_path / "nothing-here.jpg"]
|
|
274
|
+
)
|
|
275
|
+
assert filewatch.detect_allsky() == []
|
|
276
|
+
|
|
277
|
+
|
|
230
278
|
# ---------------------------------------------------------------------------
|
|
231
279
|
# Probing must never take the report down
|
|
232
280
|
# ---------------------------------------------------------------------------
|
|
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
|
|
File without changes
|
|
File without changes
|
{arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_errors.py
RENAMED
|
File without changes
|
{arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py
RENAMED
|
File without changes
|
|
File without changes
|
{arcsecond-3.18.0 → arcsecond-3.18.2}/tests/cloud/uploader/datafiles/test_uploader_prepare.py
RENAMED
|
File without changes
|
{arcsecond-3.18.0 → arcsecond-3.18.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
|
|
File without changes
|
|
File without changes
|