python-aidot-cameras 1.0.0b5__tar.gz → 1.0.0b7__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 (231) hide show
  1. {python_aidot_cameras-1.0.0b5/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b7}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/models.py +42 -0
  3. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/sd_listing.py +38 -8
  4. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/pyproject.toml +1 -1
  5. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  7. python_aidot_cameras-1.0.0b7/tests/test_sd_card_presence.py +94 -0
  8. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sd_listing.py +33 -0
  9. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/LICENSE +0 -0
  10. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/README.md +0 -0
  12. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/__init__.py +0 -0
  13. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/__main__.py +0 -0
  14. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_upstream.py +0 -0
  15. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/__init__.py +0 -0
  16. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  17. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  18. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  20. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  21. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  22. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  23. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  24. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  25. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  26. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  27. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  28. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  29. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  30. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  31. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  32. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  33. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  34. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  35. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  36. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  37. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  38. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  39. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  40. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  41. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  42. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  43. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  44. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  45. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  46. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  47. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  48. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/ble_gateway.py +0 -0
  49. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/__init__.py +0 -0
  50. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/client.py +0 -0
  51. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/constants.py +0 -0
  52. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/controls.py +0 -0
  53. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/go2rtc.py +0 -0
  54. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/hwaccel.py +0 -0
  55. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/lan_control.py +0 -0
  56. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/playback.py +0 -0
  57. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/protocol.py +0 -0
  58. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/sd_events.py +0 -0
  59. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/sdes.py +0 -0
  60. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/sdes_open.py +0 -0
  61. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/tutk.py +0 -0
  62. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/webrtc.py +0 -0
  63. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/camera/webrtc_open.py +0 -0
  64. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/client.py +0 -0
  65. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/const.py +0 -0
  66. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/credentials.py +0 -0
  67. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/crypto.py +0 -0
  68. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/device_client.py +0 -0
  69. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/discover.py +0 -0
  70. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/exceptions.py +0 -0
  71. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/g711.py +0 -0
  72. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/lan_retry.py +0 -0
  73. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/aidot_cameras/py.typed +0 -0
  74. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  75. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  76. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/python_aidot_cameras.egg-info/requires.txt +0 -0
  77. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  78. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/setup.cfg +0 -0
  79. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_aioice_compat.py +0 -0
  80. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_alarm_event.py +0 -0
  81. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_answer_sdp_video_pt.py +0 -0
  82. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_audio_mux_offline.py +0 -0
  83. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_avio_request.py +0 -0
  84. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_avio_response_parse.py +0 -0
  85. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_avio_response_router.py +0 -0
  86. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_backoff.py +0 -0
  87. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_battery_detection.py +0 -0
  88. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_battery_evidence_corroboration.py +0 -0
  89. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_battery_skip_turn.py +0 -0
  90. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_bitrate_campaign_arms.py +0 -0
  91. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_ble_gateway.py +0 -0
  92. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_bridge_state_sentinels.py +0 -0
  93. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_busy_backoff.py +0 -0
  94. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_cloud_count.py +0 -0
  95. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_cloud_plan.py +0 -0
  96. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_constants_imports_resolve.py +0 -0
  97. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_control_verdicts.py +0 -0
  98. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_deferred_hardening.py +0 -0
  99. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_device_cmd_seq_match.py +0 -0
  100. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_device_login_guard.py +0 -0
  101. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_device_user_info_cache.py +0 -0
  102. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dormant_camera_cleanup.py +0 -0
  103. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_mux_terminal_write.py +0 -0
  104. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_not_ready_burst.py +0 -0
  105. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_pinning.py +0 -0
  106. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_resolution_ack.py +0 -0
  107. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_serve_decode_skip.py +0 -0
  108. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_serve_open_timeout.py +0 -0
  109. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_skip_signaling_wait.py +0 -0
  110. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_slow_probe.py +0 -0
  111. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_dtls_stdout_producer.py +0 -0
  112. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_egress_guard.py +0 -0
  113. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_env_seams.py +0 -0
  114. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  115. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_futile_keepalive.py +0 -0
  116. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  117. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_go2rtc.py +0 -0
  118. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_go2rtc_cli.py +0 -0
  119. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_h264_decode_rate_limit.py +0 -0
  120. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_highport_nomination.py +0 -0
  121. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_hwaccel_probe.py +0 -0
  122. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_ice_config_cache.py +0 -0
  123. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_ice_peer_reflexive.py +0 -0
  124. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_idle_release_both_loops.py +0 -0
  125. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_keepalive_renew.py +0 -0
  126. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_key_restart_sdp.py +0 -0
  127. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_keyframe_prompter.py +0 -0
  128. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_lan_control.py +0 -0
  129. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  130. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_light_active_color_mode.py +0 -0
  131. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_live_session_carries_talk.py +0 -0
  132. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_live_stream_param.py +0 -0
  133. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_live_validate_policy.py +0 -0
  134. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_logging_caps.py +0 -0
  135. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_login_connect_deadline.py +0 -0
  136. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_login_reconnect_backoff.py +0 -0
  137. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_media_counted_only_when_plaintext.py +0 -0
  138. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_media_socket_rcvbuf.py +0 -0
  139. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_media_wait_floor.py +0 -0
  140. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_motion_poll.py +0 -0
  141. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_mqtt_credential_refetch.py +0 -0
  142. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_mqtt_credential_self_heal.py +0 -0
  143. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_mqtt_password_not_persisted.py +0 -0
  144. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_mqtt_session_reconnect.py +0 -0
  145. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_narrow_pc_ice.py +0 -0
  146. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_no_key_material_in_logs.py +0 -0
  147. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_no_self_referential_go2rtc.py +0 -0
  148. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_no_undefined_names.py +0 -0
  149. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  150. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_offline_keepalive_pause.py +0 -0
  151. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_open_fail_logger.py +0 -0
  152. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_open_gate_delay.py +0 -0
  153. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_peerid_reuse.py +0 -0
  154. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_persistent_mqtt.py +0 -0
  155. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_persistent_mqtt_robustness.py +0 -0
  156. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_playback_tls.py +0 -0
  157. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_post_merge_hardening.py +0 -0
  158. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  159. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_quality_campaign_in_session.py +0 -0
  160. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_recent_recordings.py +0 -0
  161. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_remb.py +0 -0
  162. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_resolution_persists.py +0 -0
  163. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_restart_cycle.py +0 -0
  164. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_retry_policy.py +0 -0
  165. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_scripts_importable.py +0 -0
  166. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sctp_init_ack_tsn.py +0 -0
  167. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sd_event_decode.py +0 -0
  168. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sd_event_probe_payloads.py +0 -0
  169. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_adaptive.py +0 -0
  170. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_avio_dispatch.py +0 -0
  171. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_bridge_break_gate.py +0 -0
  172. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_echo_wait_timeout.py +0 -0
  173. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_fast_liveplay.py +0 -0
  174. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  175. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_first_media_stall_report.py +0 -0
  176. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_idle_release.py +0 -0
  177. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_offer_video_order.py +0 -0
  178. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_offer_video_pin.py +0 -0
  179. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_open_cleanup.py +0 -0
  180. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_sctp_abort.py +0 -0
  181. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_serve_audio.py +0 -0
  182. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_serve_cmd.py +0 -0
  183. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_serve_stderr_drain.py +0 -0
  184. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_sprop.py +0 -0
  185. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_talk.py +0 -0
  186. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_teardown_exit_log.py +0 -0
  187. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_terminal_ack.py +0 -0
  188. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_terminal_ack_latency.py +0 -0
  189. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdes_watchdog.py +0 -0
  190. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdp_narrowing_properties.py +0 -0
  191. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sdp_payload_type_narrowing.py +0 -0
  192. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_security_hardening.py +0 -0
  193. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  194. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_serializable_login_info.py +0 -0
  195. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_serve_loss_tolerance.py +0 -0
  196. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_serve_relay.py +0 -0
  197. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_session_stats.py +0 -0
  198. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_shared_house_seam.py +0 -0
  199. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_snapshot_cancellation.py +0 -0
  200. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_speak.py +0 -0
  201. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sprop_capture_gate.py +0 -0
  202. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sprop_staleness.py +0 -0
  203. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  204. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_srtp_rx_rekey.py +0 -0
  205. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_srtp_tx_key_note.py +0 -0
  206. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  207. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  208. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  209. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  210. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  211. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stream_cap.py +0 -0
  212. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stream_drain_ownership.py +0 -0
  213. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stream_idle.py +0 -0
  214. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stream_slot_cap.py +0 -0
  215. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_stream_teardown.py +0 -0
  216. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_talk.py +0 -0
  217. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  218. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_terminal_ack.py +0 -0
  219. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_token_file_robustness.py +0 -0
  220. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_token_refresh.py +0 -0
  221. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_turn_create_permission.py +0 -0
  222. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  223. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_upstream_compat.py +0 -0
  224. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_video_profile_log.py +0 -0
  225. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_viewer_check_throttle.py +0 -0
  226. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_viewer_presence.py +0 -0
  227. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_viewer_present_push_mode.py +0 -0
  228. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_wait_or_event.py +0 -0
  229. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_wake_gate_camera_evidence.py +0 -0
  230. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_wake_readiness_retry.py +0 -0
  231. {python_aidot_cameras-1.0.0b5 → python_aidot_cameras-1.0.0b7}/tests/test_webrtc_answer_race.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b5
3
+ Version: 1.0.0b7
4
4
  Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
5
5
  Author-email: cbrightly <chris.brightly@gmail.com>
6
6
  License-Expression: MIT
@@ -48,6 +48,38 @@ def _as_bool(v: Any) -> Optional[bool]:
48
48
  return None if i is None else bool(i)
49
49
 
50
50
 
51
+ def _card_present(attr: dict) -> Optional[bool]:
52
+ """Is an SD card in the slot? None means nobody said.
53
+
54
+ Three states, and the third is the point. Of seven cameras measured
55
+ 2026-08-12, one reported ``SDcardExistFlag: true``, two reported ``false``,
56
+ and four carried neither key - including an A000088, the same model as all
57
+ three that did report. So a missing key cannot mean "this model cannot say";
58
+ it means we do not know, and anything that reads it as "no card" would tell
59
+ someone with a working camera that their slot is empty.
60
+
61
+ ``SDcardExistFlag`` wins where it exists. ``SDcardBaseInfo`` is the fallback
62
+ and arrives as a STRING holding a JSON array - element 0 agreed with the
63
+ flag on every camera carrying both. The other four positions are not
64
+ decoded: ``30432`` is plausibly a 32 GB card's total MB and plausibly
65
+ something else, and a guess does not belong in a public data model.
66
+ """
67
+ if "SDcardExistFlag" in attr:
68
+ flag = _as_bool(attr.get("SDcardExistFlag"))
69
+ if flag is not None:
70
+ return flag
71
+
72
+ raw = attr.get("SDcardBaseInfo")
73
+ if isinstance(raw, str):
74
+ try:
75
+ raw = json.loads(raw)
76
+ except ValueError:
77
+ return None
78
+ if isinstance(raw, (list, tuple)) and raw:
79
+ return _as_bool(raw[0])
80
+ return None
81
+
82
+
51
83
  class CameraStatusData(DeviceStatusData):
52
84
  """Core status plus camera fields; accepts both typed-model and dict updates."""
53
85
 
@@ -74,6 +106,11 @@ class CameraStatusData(DeviceStatusData):
74
106
  battery_remaining: Optional[int] = None # Battery_remaining 0-100 (%)
75
107
  occupancy: Optional[bool] = None # Occupancy live presence
76
108
  sd_card_status: Optional[str] = None # SDcardStatus
109
+ # Tri-state: True in the slot, False slot empty, None nobody said. Reads
110
+ # INVERTED on A000088 relative to sd_card_status ("0" with a card, "1"
111
+ # without, 3/3) - which is why sd_card_status is not used for this and the
112
+ # two are kept apart. That inversion has no corroboration off A000088.
113
+ sd_card_present: Optional[bool] = None # SDcardExistFlag/SDcardBaseInfo[0]
77
114
  wifi_rssi: Optional[int] = None # networkRssi (dBm), cloud property
78
115
 
79
116
  def update(self, attr) -> None:
@@ -125,6 +162,11 @@ class CameraStatusData(DeviceStatusData):
125
162
  self.occupancy = b
126
163
  if (v := attr.get("SDcardStatus")) is not None:
127
164
  self.sd_card_status = str(v)
165
+ # Only overwrite on an actual reading. Attribute pushes are partial - a
166
+ # motion notif carries no card keys - so a silent update must leave what
167
+ # the last full poll established alone.
168
+ if (b := _card_present(attr)) is not None:
169
+ self.sd_card_present = b
128
170
  if (i := _as_int(attr.get("networkRssi"))) is not None:
129
171
  self.wifi_rssi = i
130
172
 
@@ -35,9 +35,42 @@ _LOGGER = logging.getLogger(__name__)
35
35
  _SD_LIST_TIMEOUT_S = 8.0
36
36
 
37
37
 
38
+ def _session_is_live(session: Any) -> bool:
39
+ """Is this session object one that could still carry a request?
40
+
41
+ Only SDES sessions publish ``is_alive``, so a session already torn down is
42
+ caught on that transport and not on DTLS. That asymmetry is fine here and
43
+ must stay fine at every caller: this is a cheap early-out, never the thing
44
+ that keeps a dead channel from being reported as an empty card. ``answered``
45
+ does that, on every transport.
46
+ """
47
+ if session is None:
48
+ return False
49
+ return getattr(session, "is_alive", True) is not False
50
+
51
+
38
52
  class _CameraSdMixin:
39
53
  """The on-device recording query, mixed into CameraMixin."""
40
54
 
55
+ @property
56
+ def has_live_session(self) -> bool:
57
+ """Is a stream session up right now?
58
+
59
+ Public because callers legitimately need to WAIT for a session without
60
+ asking it anything. ``start_keepalive`` returns before the handshake it
61
+ schedules has finished, so a caller that wants to ride the session it
62
+ just asked for has no way to know when that session has arrived.
63
+ Probing with a real request is not a substitute: once the session is up,
64
+ the probe IS the request, and a caller that wanted to send one request
65
+ has then sent two.
66
+
67
+ Best-effort, in the direction that keeps callers honest: a torn-down
68
+ session reads as live on a transport that does not say otherwise, so a
69
+ listing taken on the strength of this can still fail and every caller
70
+ must still handle that.
71
+ """
72
+ return _session_is_live(getattr(self, "_stream_session", None))
73
+
41
74
  async def _sd_ask(self, session: Any, cmd: int, payload: bytes,
42
75
  response_cmd: int, timeout: float) -> Optional[Any]:
43
76
  """One request, with every failure flattened to None."""
@@ -84,14 +117,11 @@ class _CameraSdMixin:
84
117
  """
85
118
  session = session if session is not None else getattr(
86
119
  self, "_stream_session", None)
87
- if session is None:
88
- return None
89
- # A cheap early-out for a session already known to be torn down: there
90
- # is no point spending two timeouts on it. Only SDES sessions publish
91
- # `is_alive`, so this catches some dead sessions and not others - which
92
- # is fine, because it is NOT what keeps a dead session from being
93
- # reported as an empty card. `answered` does that, on every transport.
94
- if getattr(session, "is_alive", True) is False:
120
+ # No session is "could not ask", and so is a session already known to
121
+ # be torn down: there is no point spending two timeouts on one. Shared
122
+ # with `has_live_session` so a caller waiting for a session to exist and
123
+ # this call deciding it has one cannot come to different answers.
124
+ if not _session_is_live(session):
95
125
  return None
96
126
 
97
127
  end_ts = time.time()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0b5"
7
+ version = "1.0.0b7"
8
8
  description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b5
3
+ Version: 1.0.0b7
4
4
  Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
5
5
  Author-email: cbrightly <chris.brightly@gmail.com>
6
6
  License-Expression: MIT
@@ -160,6 +160,7 @@ tests/test_restart_cycle.py
160
160
  tests/test_retry_policy.py
161
161
  tests/test_scripts_importable.py
162
162
  tests/test_sctp_init_ack_tsn.py
163
+ tests/test_sd_card_presence.py
163
164
  tests/test_sd_event_decode.py
164
165
  tests/test_sd_event_probe_payloads.py
165
166
  tests/test_sd_listing.py
@@ -0,0 +1,94 @@
1
+ """Is there a card in the slot, and do we actually know?
2
+
3
+ A camera with no SD card and a camera that could not be asked look identical to
4
+ every listing call: both return nothing. The cloud has been carrying the answer
5
+ all along in `SDcardExistFlag` / `SDcardBaseInfo`, and nothing read it - so a
6
+ user with an empty slot was told "the camera did not answer", which is false and
7
+ not actionable.
8
+
9
+ The whole value of the field is in the THIRD state. Two of the fleet's seven
10
+ cameras report the flag as false, one reports true, and four carry neither key -
11
+ including an A000088, the same model as all three that do report. So absence
12
+ cannot mean "this model cannot report it", and anything that collapses unknown
13
+ into "no card" would tell someone with a working camera it has no card.
14
+ """
15
+ from aidot_cameras.camera.models import CameraStatusData
16
+
17
+
18
+ def test_unknown_by_default():
19
+ assert CameraStatusData().sd_card_present is None
20
+
21
+
22
+ def test_a_card_that_is_there():
23
+ s = CameraStatusData()
24
+ s.update({"SDcardExistFlag": True})
25
+ assert s.sd_card_present is True
26
+
27
+
28
+ def test_a_slot_that_is_empty():
29
+ s = CameraStatusData()
30
+ s.update({"SDcardExistFlag": False})
31
+ assert s.sd_card_present is False
32
+
33
+
34
+ def test_a_camera_that_says_nothing_stays_unknown():
35
+ # The regression that matters. Four of seven cameras look like this, and
36
+ # `False` here would put "there is no SD card in this camera" under a
37
+ # camera nobody has any evidence about.
38
+ s = CameraStatusData()
39
+ s.update({"Battery_remaining": "80", "SDcardStatus": "1"})
40
+ assert s.sd_card_present is None
41
+
42
+
43
+ def test_the_base_info_answers_when_the_flag_is_absent():
44
+ # Measured shape: a STRING holding a JSON array, not an array. Element 0
45
+ # agreed with the flag on every camera that carried both.
46
+ s = CameraStatusData()
47
+ s.update({"SDcardBaseInfo": "[true,30432,0,1,0]"})
48
+ assert s.sd_card_present is True
49
+ s2 = CameraStatusData()
50
+ s2.update({"SDcardBaseInfo": "[false,0,0,0,0]"})
51
+ assert s2.sd_card_present is False
52
+
53
+
54
+ def test_the_base_info_is_also_accepted_as_a_real_list():
55
+ # The same keys arrive in different shapes across cameras - the neighbouring
56
+ # SdcardRecord_Enable is "1" on five and true on one - so shape is not
57
+ # something to rely on.
58
+ s = CameraStatusData()
59
+ s.update({"SDcardBaseInfo": [True, 30432, 0, 1, 0]})
60
+ assert s.sd_card_present is True
61
+
62
+
63
+ def test_the_flag_wins_over_the_base_info():
64
+ s = CameraStatusData()
65
+ s.update({"SDcardExistFlag": False, "SDcardBaseInfo": "[true,30432,0,1,0]"})
66
+ assert s.sd_card_present is False
67
+
68
+
69
+ def test_a_reply_that_cannot_be_read_is_unknown_not_absent():
70
+ # A decode failure is not evidence about the slot. Silence about a card and
71
+ # a card that is not there are the two answers this whole subsystem exists
72
+ # to keep apart.
73
+ for junk in ("", "[", "not json", "[]", None, 5, {}):
74
+ s = CameraStatusData()
75
+ s.update({"SDcardBaseInfo": junk})
76
+ assert s.sd_card_present is None, junk
77
+
78
+
79
+ def test_a_later_silent_update_does_not_clobber_a_known_reading():
80
+ # Camera attribute pushes are partial: a notif about motion carries no card
81
+ # keys, and must not erase what the last full poll established.
82
+ s = CameraStatusData()
83
+ s.update({"SDcardExistFlag": True})
84
+ s.update({"Occupancy": True})
85
+ assert s.sd_card_present is True
86
+
87
+
88
+ def test_it_arrives_through_the_cloud_properties_path_too():
89
+ # Both shapes reach the same parser: setDevAttrNotif attrs and the cloud
90
+ # device record's `properties`. The probe read it off the latter.
91
+ s = CameraStatusData()
92
+ s.update_from_camera_attributes(
93
+ {"SDcardExistFlag": False, "SDcardBaseInfo": "[false,0,0,0,0]"})
94
+ assert s.sd_card_present is False
@@ -58,6 +58,39 @@ class _Camera(_CameraSdMixin):
58
58
  self.device_id = "dev-1"
59
59
 
60
60
 
61
+ def test_no_session_is_not_a_live_session():
62
+ # The point of the predicate: a caller can wait for a session to exist
63
+ # without sending anything to find out whether it does.
64
+ assert _Camera().has_live_session is False
65
+
66
+
67
+ def test_a_session_that_exists_is_a_live_session():
68
+ assert _Camera(_Session({})).has_live_session is True
69
+
70
+
71
+ def test_a_torn_down_session_is_not_a_live_session():
72
+ # Only SDES sessions say so, which is why a caller that acts on this still
73
+ # has to handle a listing that fails anyway.
74
+ assert _Camera(_Session({}, alive=False)).has_live_session is False
75
+
76
+
77
+ def test_the_predicate_reaches_the_client_a_caller_actually_holds():
78
+ # Every test above builds the mixin directly, which proves the rule and not
79
+ # the wiring. A caller reads this off the client it was handed, and reads it
80
+ # through hasattr - so a name shadowed anywhere in that MRO would not raise,
81
+ # it would quietly answer "cannot ask" and put the caller straight back to
82
+ # the behaviour this predicate exists to end.
83
+ from aidot_cameras.device_client import CameraDeviceClient, LightDeviceClient
84
+
85
+ client = object.__new__(CameraDeviceClient)
86
+ assert client.has_live_session is False
87
+ client._stream_session = _Session({})
88
+ assert client.has_live_session is True
89
+ # And it stays off the client a light gets, like the rest of the camera
90
+ # surface: that separation is enforced by construction, not by convention.
91
+ assert not hasattr(LightDeviceClient, "has_live_session")
92
+
93
+
61
94
  @pytest.mark.asyncio
62
95
  async def test_no_session_is_not_an_empty_card():
63
96
  # None, not []. A caller cannot tell "could not ask" from "nothing there"