pyobs-core 2.2.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/PKG-INFO +1 -1
  2. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/dummycamera.py +21 -2
  3. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/focus/focusseries.py +2 -2
  4. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/image/imagewatcher.py +101 -32
  5. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/module.py +2 -2
  6. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/scheduler.py +76 -24
  7. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/roof/dummyroof.py +1 -1
  8. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/telescope/basetelescope.py +5 -2
  9. pyobs_core-2.4.0/pyobs/robotic/instruments.py +195 -0
  10. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/astroplanscheduler.py +11 -1
  11. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/ondemandscheduler.py +97 -21
  12. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/taskscheduler.py +7 -1
  13. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/calibration/darkbias.py +12 -1
  14. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/calibration/pointing.py +4 -2
  15. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/imaging/autofocus.py +4 -2
  16. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/imaging/imaging.py +53 -4
  17. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/filesystem/observationarchive.py +6 -0
  18. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/portal/observationarchive.py +50 -14
  19. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/portal/taskarchive.py +62 -4
  20. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/taskarchive.py +8 -0
  21. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/task.py +12 -2
  22. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/pipeline/reduction.py +9 -4
  23. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/serialization.py +49 -3
  24. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/file.py +12 -0
  25. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/localfile.py +31 -0
  26. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/vfs.py +20 -1
  27. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyproject.toml +1 -1
  28. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/.gitignore +0 -0
  29. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/LICENSE +0 -0
  30. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/__init__.py +0 -0
  31. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/application.py +0 -0
  32. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/background_task.py +0 -0
  33. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/cli/__init__.py +0 -0
  34. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/cli/_cli.py +0 -0
  35. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/cli/pyobs.py +0 -0
  36. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/cli/pyobsd.py +0 -0
  37. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/cli/pyobsw.py +0 -0
  38. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/__init__.py +0 -0
  39. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/comm.py +0 -0
  40. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/commlogging.py +0 -0
  41. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/dummy/__init__.py +0 -0
  42. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/dummy/dummycomm.py +0 -0
  43. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/local/__init__.py +0 -0
  44. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/local/localcomm.py +0 -0
  45. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/local/localnetwork.py +0 -0
  46. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/proxy.py +0 -0
  47. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/__init__.py +0 -0
  48. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/rpc.py +0 -0
  49. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/serializer.py +0 -0
  50. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009/__init__.py +0 -0
  51. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009/binding.py +0 -0
  52. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009/rpc.py +0 -0
  53. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009_timeout/__init__.py +0 -0
  54. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009_timeout/rpc.py +0 -0
  55. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009_timeout/stanza/RPC.py +0 -0
  56. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xep_0009_timeout/stanza/__init__.py +0 -0
  57. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xmppclient.py +0 -0
  58. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/comm/xmpp/xmppcomm.py +0 -0
  59. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/__init__.py +0 -0
  60. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/badweather.py +0 -0
  61. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/event.py +0 -0
  62. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/exposurestatuschanged.py +0 -0
  63. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/filterchanged.py +0 -0
  64. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/focusfound.py +0 -0
  65. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/goodweather.py +0 -0
  66. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/log.py +0 -0
  67. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/modechanged.py +0 -0
  68. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/moduleclosed.py +0 -0
  69. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/moduleopened.py +0 -0
  70. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/motionstatuschanged.py +0 -0
  71. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/move.py +0 -0
  72. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/newimage.py +0 -0
  73. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/newspectrum.py +0 -0
  74. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/offsets.py +0 -0
  75. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/roofclosing.py +0 -0
  76. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/roofopened.py +0 -0
  77. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/taskfailed.py +0 -0
  78. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/taskfinished.py +0 -0
  79. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/taskstarted.py +0 -0
  80. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/events/testevent.py +0 -0
  81. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/__init__.py +0 -0
  82. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/image.py +0 -0
  83. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/__init__.py +0 -0
  84. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/altazoffsets.py +0 -0
  85. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/exptime.py +0 -0
  86. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/genericoffset.py +0 -0
  87. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/onskydistance.py +0 -0
  88. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/pixeloffsets.py +0 -0
  89. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/radecoffsets.py +0 -0
  90. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/meta/skyoffsets.py +0 -0
  91. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processor.py +0 -0
  92. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/__init__.py +0 -0
  93. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/_daobackgroundremover.py +0 -0
  94. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/annotation/__init__.py +0 -0
  95. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/annotation/_pillow.py +0 -0
  96. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/annotation/circle.py +0 -0
  97. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/annotation/crosshair.py +0 -0
  98. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/annotation/text.py +0 -0
  99. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/__init__.py +0 -0
  100. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/_dotnet_request.py +0 -0
  101. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/_dotnet_request_builder.py +0 -0
  102. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/_dotnet_request_logger.py +0 -0
  103. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/_dotnet_response_saver.py +0 -0
  104. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/astrometry.py +0 -0
  105. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/astrometry/dotnet.py +0 -0
  106. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/calibration/__init__.py +0 -0
  107. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/calibration/_calibration_cache.py +0 -0
  108. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/calibration/_ccddata_calibrator.py +0 -0
  109. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/calibration/calibration.py +0 -0
  110. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/__init__.py +0 -0
  111. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/_pysep_stats_calculator.py +0 -0
  112. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/_source_catalog.py +0 -0
  113. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/daophot.py +0 -0
  114. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/pysep.py +0 -0
  115. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/simpledisk.py +0 -0
  116. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/detection/sourcedetection.py +0 -0
  117. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/exptime/__init__.py +0 -0
  118. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/exptime/exptime.py +0 -0
  119. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/exptime/star.py +0 -0
  120. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/__init__.py +0 -0
  121. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/addfitsheaders.py +0 -0
  122. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/download.py +0 -0
  123. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/flip.py +0 -0
  124. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/grayscale.py +0 -0
  125. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/httpserver.py +0 -0
  126. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/normalize.py +0 -0
  127. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/save.py +0 -0
  128. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/saveimage.py +0 -0
  129. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/smooth.py +0 -0
  130. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/image/softbin.py +0 -0
  131. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/__init__.py +0 -0
  132. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/addmask.py +0 -0
  133. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/catalogcircularmask.py +0 -0
  134. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/circularmask.py +0 -0
  135. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/createfilename.py +0 -0
  136. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/imagesourcefilter.py +0 -0
  137. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/misc/removebackground.py +0 -0
  138. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/modules/__init__.py +0 -0
  139. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/modules/getfitsheaders.py +0 -0
  140. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/__init__.py +0 -0
  141. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/astrometry.py +0 -0
  142. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/brighteststar.py +0 -0
  143. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/brighteststar_guiding.py +0 -0
  144. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/dummyoffsets.py +0 -0
  145. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/dummyskyoffsets.py +0 -0
  146. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/fitsheader.py +0 -0
  147. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/offsets.py +0 -0
  148. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/pixeloffsets.py +0 -0
  149. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/projected.py +0 -0
  150. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/offsets/spilled_light.py +0 -0
  151. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/__init__.py +0 -0
  152. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/_photometry_calculator.py +0 -0
  153. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/_photutil_aperture_photometry.py +0 -0
  154. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/_sep_aperture_photometry.py +0 -0
  155. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/aperture_photometry.py +0 -0
  156. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/photometry.py +0 -0
  157. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/photutil.py +0 -0
  158. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/photometry/pysep.py +0 -0
  159. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/wcs/__init__.py +0 -0
  160. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/images/processors/wcs/solarhelioprojective.py +0 -0
  161. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IAbortable.py +0 -0
  162. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IAcquisition.py +0 -0
  163. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IAutoFocus.py +0 -0
  164. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IAutoGuiding.py +0 -0
  165. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IAutonomous.py +0 -0
  166. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IBinning.py +0 -0
  167. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ICalibrate.py +0 -0
  168. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ICamera.py +0 -0
  169. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IConfig.py +0 -0
  170. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ICooling.py +0 -0
  171. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IData.py +0 -0
  172. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IDataSequence.py +0 -0
  173. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IDome.py +0 -0
  174. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IExposure.py +0 -0
  175. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IExposureTime.py +0 -0
  176. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IFilters.py +0 -0
  177. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IFitsHeaderAfter.py +0 -0
  178. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IFitsHeaderBefore.py +0 -0
  179. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IFlatField.py +0 -0
  180. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IFocusModel.py +0 -0
  181. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IFocuser.py +0 -0
  182. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IGain.py +0 -0
  183. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IImageFormat.py +0 -0
  184. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IImageType.py +0 -0
  185. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IMode.py +0 -0
  186. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IModule.py +0 -0
  187. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IMotion.py +0 -0
  188. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IMultiFiber.py +0 -0
  189. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IOffsetsAltAz.py +0 -0
  190. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IOffsetsRaDec.py +0 -0
  191. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingAltAz.py +0 -0
  192. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingBody.py +0 -0
  193. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingHeliocentricPolar.py +0 -0
  194. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingHeliographicStonyhurst.py +0 -0
  195. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingHelioprojective.py +0 -0
  196. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingOrbitalElements.py +0 -0
  197. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingRaDec.py +0 -0
  198. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IPointingSeries.py +0 -0
  199. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IReady.py +0 -0
  200. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IRobotic.py +0 -0
  201. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IRoboticScheduler.py +0 -0
  202. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IRoof.py +0 -0
  203. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IRotation.py +0 -0
  204. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IRunnable.py +0 -0
  205. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IRunning.py +0 -0
  206. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IScriptRunner.py +0 -0
  207. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ISpectrograph.py +0 -0
  208. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IStartStop.py +0 -0
  209. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IStructuredConfig.py +0 -0
  210. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ISyncTarget.py +0 -0
  211. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ITelescope.py +0 -0
  212. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ITemperatures.py +0 -0
  213. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ITrackingMode.py +0 -0
  214. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/ITrackingRate.py +0 -0
  215. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IVideo.py +0 -0
  216. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IWeather.py +0 -0
  217. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/IWindow.py +0 -0
  218. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/__init__.py +0 -0
  219. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/interfaces/interface.py +0 -0
  220. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/__init__.py +0 -0
  221. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/camerasettings.py +0 -0
  222. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/fitsheader.py +0 -0
  223. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/fitsnamespace.py +0 -0
  224. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/follow.py +0 -0
  225. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/motionstatus.py +0 -0
  226. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/pipeline.py +0 -0
  227. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/waitformotion.py +0 -0
  228. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/mixins/weatheraware.py +0 -0
  229. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/__init__.py +0 -0
  230. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/__init__.py +0 -0
  231. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/adaptive.py +0 -0
  232. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/basecamera.py +0 -0
  233. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/basespectrograph.py +0 -0
  234. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/basevideo.py +0 -0
  235. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/dummyspectrograph.py +0 -0
  236. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/dummyvideo.py +0 -0
  237. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/camera/pipelinecamera.py +0 -0
  238. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/flatfield/__init__.py +0 -0
  239. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/flatfield/flatfield.py +0 -0
  240. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/flatfield/pointing.py +0 -0
  241. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/flatfield/scheduler.py +0 -0
  242. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/focus/__init__.py +0 -0
  243. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/focus/dummyautofocus.py +0 -0
  244. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/focus/focusmodel.py +0 -0
  245. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/image/__init__.py +0 -0
  246. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/image/imagewriter.py +0 -0
  247. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/image/pipeline.py +0 -0
  248. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/image/seeing.py +0 -0
  249. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/__init__.py +0 -0
  250. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/_base.py +0 -0
  251. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/_baseguiding.py +0 -0
  252. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/acquisition.py +0 -0
  253. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/autoguiding.py +0 -0
  254. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/dummyacquisition.py +0 -0
  255. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/dummyguiding.py +0 -0
  256. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/guidingstatistics/__init__.py +0 -0
  257. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/guidingstatistics/guidingstatistics.py +0 -0
  258. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/guidingstatistics/pixeloffset.py +0 -0
  259. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/guidingstatistics/skyoffsets.py +0 -0
  260. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/guidingstatistics/uptime.py +0 -0
  261. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/pointing/scienceframeguiding.py +0 -0
  262. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/__init__.py +0 -0
  263. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/dummymastermind.py +0 -0
  264. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/dummyscheduler.py +0 -0
  265. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/mastermind.py +0 -0
  266. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/pointing.py +0 -0
  267. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/robotic/scriptrunner.py +0 -0
  268. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/roof/__init__.py +0 -0
  269. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/roof/basedome.py +0 -0
  270. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/roof/baseroof.py +0 -0
  271. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/telescope/__init__.py +0 -0
  272. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/telescope/_dummytelescopebase.py +0 -0
  273. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/telescope/dummyaltaztelescope.py +0 -0
  274. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/telescope/dummyradectelescope.py +0 -0
  275. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/telescope/dummysolartelescope.py +0 -0
  276. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/test/__init__.py +0 -0
  277. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/test/standalone.py +0 -0
  278. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/__init__.py +0 -0
  279. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/dummymode.py +0 -0
  280. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/dummystructuredconfig.py +0 -0
  281. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/fluentlogger.py +0 -0
  282. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/httpfilecache.py +0 -0
  283. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/kiosk.py +0 -0
  284. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/matrix.py +0 -0
  285. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/stellarium.py +0 -0
  286. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/telegram.py +0 -0
  287. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/utils/trigger.py +0 -0
  288. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/weather/__init__.py +0 -0
  289. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/weather/mockweather.py +0 -0
  290. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/weather/weather.py +0 -0
  291. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/weather/weather_api.py +0 -0
  292. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/modules/weather/weather_state.py +0 -0
  293. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/object.py +0 -0
  294. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/py.typed +0 -0
  295. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/__init__.py +0 -0
  296. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/observation.py +0 -0
  297. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/__init__.py +0 -0
  298. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/_executor.py +0 -0
  299. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/__init__.py +0 -0
  300. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/airmassconstraint.py +0 -0
  301. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/constraint.py +0 -0
  302. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/moonilluminationconstraint.py +0 -0
  303. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/moonseparationconstraint.py +0 -0
  304. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/solarelevationconstraint.py +0 -0
  305. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/constraints/timeconstraint.py +0 -0
  306. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/dataprovider.py +0 -0
  307. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/__init__.py +0 -0
  308. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/aftertime.py +0 -0
  309. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/beforetime.py +0 -0
  310. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/constant.py +0 -0
  311. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/follow.py +0 -0
  312. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/interval.py +0 -0
  313. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/merit.py +0 -0
  314. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/pernight.py +0 -0
  315. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/random.py +0 -0
  316. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/timewindow.py +0 -0
  317. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/merits/transit.py +0 -0
  318. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/observationarchiveevolution.py +0 -0
  319. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/__init__.py +0 -0
  320. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/dynamictarget.py +0 -0
  321. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/heliocentricpolartarget.py +0 -0
  322. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/helioprojectivetarget.py +0 -0
  323. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/picker/__init__.py +0 -0
  324. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/picker/csvpicker.py +0 -0
  325. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/picker/picker.py +0 -0
  326. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/siderealtarget.py +0 -0
  327. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scheduler/targets/target.py +0 -0
  328. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/__init__.py +0 -0
  329. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/calibration/__init__.py +0 -0
  330. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/calibration/skyflats.py +0 -0
  331. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/control/__init__.py +0 -0
  332. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/control/cases.py +0 -0
  333. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/control/conditional.py +0 -0
  334. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/control/parallel.py +0 -0
  335. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/control/selector.py +0 -0
  336. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/control/sequential.py +0 -0
  337. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/exceptions.py +0 -0
  338. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/imaging/__init__.py +0 -0
  339. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/imaging/transitimaging.py +0 -0
  340. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/script.py +0 -0
  341. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/utils/__init__.py +0 -0
  342. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/utils/callmodule.py +0 -0
  343. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/utils/debugtrigger.py +0 -0
  344. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/scripts/utils/log.py +0 -0
  345. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/__init__.py +0 -0
  346. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/filesystem/__init__.py +0 -0
  347. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/filesystem/taskarchive.py +0 -0
  348. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/__init__.py +0 -0
  349. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/_portal.py +0 -0
  350. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/_schedulereader.py +0 -0
  351. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/_schedulewriter.py +0 -0
  352. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/configdb.py +0 -0
  353. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/mockobservationarchive.py +0 -0
  354. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/observationarchive.py +0 -0
  355. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/scripts/__init__.py +0 -0
  356. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/scripts/autofocus.py +0 -0
  357. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/scripts/default.py +0 -0
  358. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/scripts/script.py +0 -0
  359. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/task.py +0 -0
  360. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/taskarchive.py +0 -0
  361. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/lco/taskrunner.py +0 -0
  362. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/memory/__init__.py +0 -0
  363. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/memory/observationarchive.py +0 -0
  364. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/memory/taskarchive.py +0 -0
  365. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/observationarchive.py +0 -0
  366. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/storage/portal/__init__.py +0 -0
  367. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/taskrunner.py +0 -0
  368. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/__init__.py +0 -0
  369. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/archive/__init__.py +0 -0
  370. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/archive/archive.py +0 -0
  371. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/archive/local_archive.py +0 -0
  372. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/archive/pyobs_archive.py +0 -0
  373. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/calibration.py +0 -0
  374. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/exptime/__init__.py +0 -0
  375. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/exptime/exptime.py +0 -0
  376. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/exptime/stellarexptime.py +0 -0
  377. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/__init__.py +0 -0
  378. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/exptimeeval.py +0 -0
  379. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/flatfielder.py +0 -0
  380. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/pointing/__init__.py +0 -0
  381. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/pointing/base.py +0 -0
  382. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/pointing/static.py +0 -0
  383. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/priorities/__init__.py +0 -0
  384. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/priorities/archive.py +0 -0
  385. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/priorities/base.py +0 -0
  386. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/priorities/const.py +0 -0
  387. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/robotic/utils/skyflats/scheduler.py +0 -0
  388. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/__init__.py +0 -0
  389. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/average.py +0 -0
  390. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/cache.py +0 -0
  391. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/config.py +0 -0
  392. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/config_schema.py +0 -0
  393. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/coordinates.py +0 -0
  394. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/curvefit.py +0 -0
  395. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/enums.py +0 -0
  396. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/exceptions.py +0 -0
  397. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/exptime_grouping.py +0 -0
  398. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/fits.py +0 -0
  399. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/focusseries/__init__.py +0 -0
  400. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/focusseries/base.py +0 -0
  401. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/focusseries/photometry.py +0 -0
  402. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/focusseries/projection.py +0 -0
  403. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/grids/__init__.py +0 -0
  404. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/grids/filters.py +0 -0
  405. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/grids/grid.py +0 -0
  406. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/grids/gridnode.py +0 -0
  407. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/grids/pipeline.py +0 -0
  408. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/__init__.py +0 -0
  409. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/__init__.py +0 -0
  410. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/binningwidget.py +0 -0
  411. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/datadisplaywidget.py +0 -0
  412. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/exposewidget.py +0 -0
  413. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/exposuretimewidget.py +0 -0
  414. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/imageformatwidget.py +0 -0
  415. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/listpickerdialog.py +0 -0
  416. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/gui/camera/windowingwidget.py +0 -0
  417. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/http.py +0 -0
  418. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/influxdb.py +0 -0
  419. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/logger.py +0 -0
  420. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/logging/__init__.py +0 -0
  421. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/logging/context.py +0 -0
  422. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/logging/resolvableerror.py +0 -0
  423. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/modulegui.py +0 -0
  424. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/offsets/__init__.py +0 -0
  425. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/offsets/applyaltazoffsets.py +0 -0
  426. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/offsets/applyoffsets.py +0 -0
  427. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/offsets/applyradecoffsets.py +0 -0
  428. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/parallel.py +0 -0
  429. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/pipeline/__init__.py +0 -0
  430. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/pipeline/pipeline.py +0 -0
  431. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/pipeline/progress.py +0 -0
  432. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/pipeline/reduction_base.py +0 -0
  433. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/publisher/__init__.py +0 -0
  434. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/publisher/csv.py +0 -0
  435. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/publisher/http.py +0 -0
  436. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/publisher/log.py +0 -0
  437. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/publisher/multi.py +0 -0
  438. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/publisher/publisher.py +0 -0
  439. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/shellcommand.py +0 -0
  440. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/threads/__init__.py +0 -0
  441. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/threads/lockwithabort.py +0 -0
  442. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/time.py +0 -0
  443. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/units.py +0 -0
  444. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/utils/versions.py +0 -0
  445. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/version.py +0 -0
  446. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/__init__.py +0 -0
  447. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/archivefile.py +0 -0
  448. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/bufferedfile.py +0 -0
  449. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/filelists/__init__.py +0 -0
  450. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/filelists/filelist.py +0 -0
  451. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/filelists/testing.py +0 -0
  452. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/httpfile.py +0 -0
  453. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/memfile.py +0 -0
  454. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/sftpfile.py +0 -0
  455. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/smbfile.py +0 -0
  456. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/sshfile.py +0 -0
  457. {pyobs_core-2.2.0 → pyobs_core-2.4.0}/pyobs/vfs/tempfile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-core
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: robotic telescope software
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -28,9 +28,11 @@ from pyobs.interfaces import (
28
28
  IImageFormat,
29
29
  ImageFormatCapabilities,
30
30
  ImageFormatState,
31
+ IMotion,
31
32
  IPointingRaDec,
32
33
  ITemperatures,
33
34
  IWindow,
35
+ MotionState,
34
36
  RaDecState,
35
37
  SensorReading,
36
38
  TemperaturesState,
@@ -39,7 +41,7 @@ from pyobs.interfaces import (
39
41
  )
40
42
  from pyobs.modules.camera.basecamera import BaseCamera
41
43
  from pyobs.utils import exceptions as exc
42
- from pyobs.utils.enums import ExposureStatus, ImageFormat, ImageType
44
+ from pyobs.utils.enums import ExposureStatus, ImageFormat, ImageType, MotionStatus
43
45
  from pyobs.utils.time import Time
44
46
 
45
47
  log = logging.getLogger(__name__)
@@ -73,6 +75,7 @@ class DummyCamera(BaseCamera, IWindow, IBinning, ICooling, IGain, IImageFormat):
73
75
  max_mag: float = 20.0,
74
76
  seeing: float = 3.0,
75
77
  telescope: str | None = None,
78
+ roof: str | None = None,
76
79
  **kwargs: Any,
77
80
  ):
78
81
  """Creates a new dummy camera.
@@ -86,6 +89,10 @@ class DummyCamera(BaseCamera, IWindow, IBinning, ICooling, IGain, IImageFormat):
86
89
  max_mag: Maximum magnitude of simulated stars.
87
90
  seeing: Seeing in arcsec FWHM.
88
91
  telescope: Name of telescope module to read pointing from (for star simulation).
92
+ roof: Name of roof module to read open/closed state from. When given, the sky (flat
93
+ flux and stars) is only simulated while the roof is not parked -- frames come
94
+ back dark otherwise. Fails closed: until the roof's state has been received, the
95
+ roof is assumed closed.
89
96
  """
90
97
  BaseCamera.__init__(self, **kwargs)
91
98
  self.add_background_task(self._cooling_thread, True)
@@ -112,6 +119,10 @@ class DummyCamera(BaseCamera, IWindow, IBinning, ICooling, IGain, IImageFormat):
112
119
  self._telescope_module = telescope
113
120
  self._telescope_pos: SkyCoord = SkyCoord(0.0 * u.deg, 0.0 * u.deg, frame="icrs")
114
121
 
122
+ # roof state (updated via subscribe_state); fail closed until a state arrives
123
+ self._roof_module = roof
124
+ self._roof_open: bool = roof is None
125
+
115
126
  # camera state
116
127
  self._cooling = CoolingStatus()
117
128
  self._exposing = True
@@ -128,6 +139,10 @@ class DummyCamera(BaseCamera, IWindow, IBinning, ICooling, IGain, IImageFormat):
128
139
  """Update cached telescope position from IPointingRaDec state."""
129
140
  self._telescope_pos = SkyCoord(ra=state.ra * u.deg, dec=state.dec * u.deg, frame="icrs")
130
141
 
142
+ def _on_roof_state(self, state: MotionState) -> None:
143
+ """Update cached roof open/closed state from IMotion state."""
144
+ self._roof_open = state.status != MotionStatus.PARKED
145
+
131
146
  async def open(self) -> None:
132
147
  """Opens camera."""
133
148
  # publish capabilities before super().open()
@@ -154,6 +169,10 @@ class DummyCamera(BaseCamera, IWindow, IBinning, ICooling, IGain, IImageFormat):
154
169
  if self._telescope_module:
155
170
  await self.comm.subscribe_state(self._telescope_module, IPointingRaDec, self._on_telescope_state)
156
171
 
172
+ # subscribe to roof state if given
173
+ if self._roof_module:
174
+ await self.comm.subscribe_state(self._roof_module, IMotion, self._on_roof_state)
175
+
157
176
  # publish initial states
158
177
  await self.comm.set_state(
159
178
  ICooling,
@@ -206,7 +225,7 @@ class DummyCamera(BaseCamera, IWindow, IBinning, ICooling, IGain, IImageFormat):
206
225
 
207
226
  if exp_time > 0:
208
227
  data += make_noise_image(shape, distribution="gaussian", mean=exp_time / 1e4, stddev=exp_time / 1e5)
209
- if open_shutter:
228
+ if open_shutter and self._roof_open:
210
229
  sun_alt = self._sun_alt()
211
230
  flat_counts = 30000 / np.exp(-1.28 * (4.209 + sun_alt)) * exp_time
212
231
  data += make_noise_image(shape, distribution="gaussian", mean=flat_counts, stddev=flat_counts / 10.0)
@@ -158,7 +158,7 @@ class AutoFocusSeries(Module, CameraSettingsMixin, IAutoFocus):
158
158
  foc_state = focuser.get_state(IFocuser)
159
159
  guess = foc_state.focus if foc_state is not None else 0.0
160
160
  log.info("Using current focus of %.2fmm as initial guess.", guess)
161
- except exc.PyobsError:
161
+ except (exc.PyobsError, ValueError):
162
162
  raise exc.FocusError("Could not fetch current focus value.")
163
163
 
164
164
  # define array of focus values to iterate
@@ -185,7 +185,7 @@ class AutoFocusSeries(Module, CameraSettingsMixin, IAutoFocus):
185
185
  else:
186
186
  await focuser.set_focus(float(foc))
187
187
 
188
- except exc.PyobsError:
188
+ except (exc.PyobsError, ValueError):
189
189
  raise exc.FocusError("Could not set new focus value.")
190
190
 
191
191
  # do exposure
@@ -5,7 +5,7 @@ import os
5
5
  import time
6
6
  import warnings
7
7
  from dataclasses import dataclass
8
- from pathlib import PurePosixPath
8
+ from pathlib import Path, PurePosixPath
9
9
  from typing import Any
10
10
 
11
11
  from astropy.io import fits
@@ -15,6 +15,12 @@ from pyobs.utils.fits import format_filename
15
15
 
16
16
  log = logging.getLogger(__name__)
17
17
 
18
+ # Filename suffixes whose content is attempted to be parsed as FITS (for templated destination
19
+ # names and derived classes' process_extra/cleanup_extra hooks). Files whose names don't match
20
+ # are still watched and copied as-is to non-templated destinations, just never handed to astropy
21
+ # -- which is what produced the header warnings on non-FITS files.
22
+ FITS_FILENAME_SUFFIXES = (".fits", ".fitz", ".fits.gz", ".fits.fz")
23
+
18
24
 
19
25
  @dataclass
20
26
  class CurrentFile:
@@ -47,6 +53,7 @@ class ImageWatcher(Module):
47
53
  poll_interval: int = 5,
48
54
  wait_time: int = 10,
49
55
  pattern: str = "*",
56
+ flatten: bool = True,
50
57
  **kwargs: Any,
51
58
  ):
52
59
  """Create a new image watcher.
@@ -59,6 +66,26 @@ class ImageWatcher(Module):
59
66
  wait_time: Time in seconds between adding a file to the list and processing it. Gives a file that is
60
67
  still being written time to finish (relevant for poll mode and the initial scan) and spaces out
61
68
  re-queued files after failed destination copies.
69
+ pattern: Only watch/process files matching this fnmatch pattern, checked against the full path (so
70
+ e.g. "*.fits" also matches files found in subdirectories). This is the single point of filtering,
71
+ applied uniformly in ``add_file`` regardless of watch mode -- inotify events, polling, and the
72
+ initial scan in ``open()`` all discover candidates first and let ``add_file`` decide, none of
73
+ them pre-filter by pattern before that.
74
+ flatten: For a non-templated destination (one without ``{placeholder}``s), whether to collapse the
75
+ file's path to just its basename under that destination (the historical behavior, default) or to
76
+ preserve its path relative to ``watchpath`` under the destination instead. Files found via
77
+ recursive watching or polling only keep their subdirectory structure at the destination when this
78
+ is False. Note this relies on the destination VFS root creating missing parent directories on
79
+ write (the default for e.g. ``LocalFile``) -- a destination configured to not do that would
80
+ repeatedly fail and re-queue any file whose relative path needs a parent directory that doesn't
81
+ exist yet.
82
+
83
+ Note:
84
+ If a directory holding a file that's already queued (added but not yet processed by ``_worker``) is
85
+ renamed before that happens, the worker's read of the file at its captured (pre-rename) path fails
86
+ and the file is dropped rather than picked up at its new location -- this is unrelated to, and not
87
+ covered by, the recursive-watching support for directories renamed after they no longer hold queued
88
+ files. Avoid renaming directories that may still hold queued/retrying files.
62
89
  """
63
90
  Module.__init__(self, **kwargs)
64
91
 
@@ -77,6 +104,7 @@ class ImageWatcher(Module):
77
104
  self._poll_interval = poll_interval
78
105
  self._wait_time = wait_time
79
106
  self._pattern = pattern
107
+ self._flatten = flatten
80
108
  self.current_file: CurrentFile | None = None
81
109
 
82
110
  # filename patterns
@@ -85,49 +113,58 @@ class ImageWatcher(Module):
85
113
  self._destinations = destinations
86
114
 
87
115
  async def _watch_inotify(self) -> None:
88
- from asyncinotify import Inotify, Mask
116
+ from asyncinotify import Mask, RecursiveWatcher
89
117
 
90
118
  # get local directory
91
119
  local = await self.vfs.local_path(self._watchpath)
92
120
 
93
- # Context manager to close the inotify handle after use
94
- with Inotify() as inotify:
95
- # add watch on local directory
96
- inotify.add_watch(local, Mask.CLOSE_WRITE)
97
-
98
- # iterate events forever
99
- async for event in inotify:
100
- # get filename by replacing local with watchpath
101
- filename = str(event.path).replace(local, self._watchpath)
121
+ # recursively watch the local directory: watches are added for subdirectories as they're
122
+ # created or moved in, and removed as they're moved out (deletion needs no explicit
123
+ # removal -- the kernel invalidates the watch on its own, see the plan doc for details).
124
+ # A directory can also vanish between an event being received and asyncinotify acting on
125
+ # it (its own add_watch/rm_watch racing a concurrent delete), which raises OSError from
126
+ # inside the library rather than being handled there. Restart with a fresh watcher (a
127
+ # full re-walk) on that instead of letting it kill this task outright -- self-heals faster
128
+ # than falling back to the module's outer background-task restart-with-backoff.
129
+ while True:
130
+ try:
131
+ watcher = RecursiveWatcher(Path(local), Mask.CLOSE_WRITE)
132
+ async for event in watcher.watch_recursive():
133
+ # get filename by replacing local with watchpath
134
+ filename = str(event.path).replace(local, self._watchpath)
102
135
 
103
- # add file
104
- await self.add_file(filename)
136
+ # add file
137
+ await self.add_file(filename)
138
+ except OSError as e:
139
+ log.warning("Inotify watch error (directory likely vanished mid-scan), restarting watch: %s", e)
105
140
 
106
141
  async def _watch_poll(self) -> None:
107
- # init list
108
- files = set(await self.vfs.listdir(self._watchpath))
142
+ # init list (recursive, so subdirectories are picked up too; unfiltered by pattern here --
143
+ # add_file is the single point of pattern filtering, same as inotify mode)
144
+ files = set(await self.vfs.find(self._watchpath, "*"))
109
145
 
110
146
  # run forever
111
- path = PurePosixPath(self._watchpath)
112
147
  while True:
148
+ await asyncio.sleep(self._poll_interval)
149
+
113
150
  # get new list
114
- new_files = await self.vfs.listdir(self._watchpath)
151
+ new_files = set(await self.vfs.find(self._watchpath, "*"))
115
152
 
116
153
  # find all new files and add them
117
- for f in new_files:
118
- if f not in files:
119
- print(str(path / f))
120
- await self.add_file(str(path / f))
154
+ for f in new_files - files:
155
+ await self.add_file(os.path.join(self._watchpath, f))
121
156
 
122
157
  # store new list
123
- files = set(new_files)
158
+ files = new_files
124
159
 
125
160
  async def open(self) -> None:
126
161
  """Open image watcher."""
127
162
  await Module.open(self)
128
163
 
129
- # add all files from directory to queue
130
- for filename in await self.vfs.listdir(self._watchpath):
164
+ # add all files from directory to queue (recursive, so subdirectories are picked up too;
165
+ # unfiltered by pattern here -- add_file is the single point of pattern filtering, same as
166
+ # inotify mode)
167
+ for filename in await self.vfs.find(self._watchpath, "*"):
131
168
  await self.add_file(os.path.join(self._watchpath, filename))
132
169
 
133
170
  async def close(self) -> None:
@@ -172,13 +209,20 @@ class ImageWatcher(Module):
172
209
  async with self.vfs.open_file(filename, "rb") as fd:
173
210
  data = await fd.read()
174
211
 
175
- # try to load as fits file; data may well not be a FITS file at all
176
- try:
177
- with warnings.catch_warnings():
178
- warnings.simplefilter("ignore", fits.verify.VerifyWarning)
179
- fits_file = await asyncio.to_thread(fits.HDUList.fromstring, data)
180
- except Exception:
181
- fits_file = None
212
+ # only attempt to load the file as FITS when its name suggests it is one; any
213
+ # other file (e.g. a raw camera binary like "w123.0") is still copied as-is to
214
+ # non-templated destinations, just without the parse attempt and the astropy
215
+ # header warnings it raises on non-FITS content. A matching name is no
216
+ # guarantee, though -- the data may be corrupt or truncated -- hence the
217
+ # try/except below.
218
+ fits_file = None
219
+ if os.path.basename(filename).lower().endswith(FITS_FILENAME_SUFFIXES):
220
+ try:
221
+ with warnings.catch_warnings():
222
+ warnings.simplefilter("ignore", fits.verify.VerifyWarning)
223
+ fits_file = await asyncio.to_thread(fits.HDUList.fromstring, data)
224
+ except Exception:
225
+ fits_file = None
182
226
 
183
227
  # fill current file
184
228
  self.current_file = CurrentFile(filename=filename, data=data, hdu_list=fits_file)
@@ -193,10 +237,15 @@ class ImageWatcher(Module):
193
237
  if out_filename is None:
194
238
  raise ValueError("Could not create name for file.")
195
239
 
196
- else:
240
+ elif self._flatten:
197
241
  # no formatting, so just add filename to destination
198
242
  out_filename = os.path.join(pattern, os.path.basename(filename))
199
243
 
244
+ else:
245
+ # preserve the file's path relative to watchpath under the destination
246
+ rel = PurePosixPath(filename).relative_to(self._watchpath)
247
+ out_filename = str(PurePosixPath(pattern) / rel)
248
+
200
249
  # store it
201
250
  log.info("Storing file as %s...", out_filename)
202
251
  self.current_file.out_filename = out_filename
@@ -223,6 +272,9 @@ class ImageWatcher(Module):
223
272
  log.info("Removing file from watch directory...")
224
273
  if not await self.vfs.remove(filename):
225
274
  log.warning("Could not delete %s.", filename)
275
+ else:
276
+ # clean up any now-empty parent directories the file left behind
277
+ await self._cleanup_empty_parents(filename)
226
278
 
227
279
  # cleanup extra
228
280
  await self.cleanup_extra(filename)
@@ -230,6 +282,23 @@ class ImageWatcher(Module):
230
282
  except Exception:
231
283
  log.exception("Something went wrong.")
232
284
 
285
+ async def _cleanup_empty_parents(self, filename: str) -> None:
286
+ """Remove now-empty parent directories left behind by a just-deleted file.
287
+
288
+ Walks from filename's immediate parent up towards (but excluding) watchpath, removing
289
+ each directory while it's empty and stopping at the first one that isn't (or otherwise
290
+ can't be removed) -- most commonly a directory that still holds sibling files/dirs, which
291
+ is the normal, expected way this loop ends. Only relevant for recursive watching
292
+ (watchpath itself is never removed); a flat watch directory has no subdirectories for
293
+ this to ever act on.
294
+ """
295
+ watchpath = PurePosixPath(self._watchpath)
296
+ parent = PurePosixPath(filename).parent
297
+ while parent != watchpath and watchpath in parent.parents:
298
+ if not await self.vfs.rmdir(str(parent)):
299
+ break
300
+ parent = parent.parent
301
+
233
302
  async def process_extra(self, filename: str) -> bool:
234
303
  """Can be overwritten by derived classes to do extra processing on files.
235
304
  All information are stored in self.current_file and can be checked against the given filename.
@@ -439,8 +439,8 @@ class Module(Object, IModule, IConfig):
439
439
  caps = proxy.get_capabilities(IModule)
440
440
  module_version = caps.version if caps is not None else ""
441
441
  remote_location = caps.location if caps is not None else None
442
- except exc.PyobsError:
443
- # however this failed (transport or domain), we just skip this module
442
+ except (exc.PyobsError, ValueError):
443
+ # however this failed (transport, domain, or unresolvable peer), we just skip this module
444
444
  return True
445
445
 
446
446
  # log it
@@ -106,13 +106,21 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
106
106
 
107
107
  # get scheduler
108
108
  self._task_archive = self.add_child_object(tasks, TaskArchive, on_tasks_changed=self._update_schedule)
109
- # PortalObservationArchive declares auto_update and gates its own polling loop on it --
110
- # since we already drive updates via on_tasks_changed above, that poller must stay off.
111
- # Other ObservationArchive implementations (e.g. LcoObservationArchive) don't declare it at
112
- # all, so it must not be injected unconditionally.
109
+ # PortalObservationArchive's own polling loop (gated on auto_update) is the only channel
110
+ # by which this process learns about observation-state changes written by *other*
111
+ # processes (e.g. Mastermind marking an observation IN_PROGRESS/COMPLETED) -- it must stay
112
+ # on, or get_schedule()/get_current_observation()/get_next_observation() never see
113
+ # anything beyond the empty list they're constructed with. What it must NOT do is announce
114
+ # every update at INFO: when *this* process is the one that just computed and posted the
115
+ # schedule, _schedule_worker already logs it in detail (see _log_scheduled_task below), so
116
+ # the poller picking up its own write ~5s later would double-log it. announce_updates=False
117
+ # suppresses that one-line echo while leaving auto_update's poll (and the INFO log for
118
+ # externally-driven changes like Mastermind's) intact. Other ObservationArchive
119
+ # implementations (e.g. LcoObservationArchive) don't declare either param, so injection
120
+ # must stay conditional, not unconditional.
113
121
  extra_schedule_kwargs: dict[str, Any] = {}
114
- if _class_accepts_param(schedule, "auto_update"):
115
- extra_schedule_kwargs["auto_update"] = False
122
+ if _class_accepts_param(schedule, "announce_updates"):
123
+ extra_schedule_kwargs["announce_updates"] = False
116
124
  self._schedule = self.add_child_object(schedule, ObservationArchive, **extra_schedule_kwargs)
117
125
  extra_scheduler_kwargs: dict[str, Any] = {}
118
126
  if _class_accepts_param(scheduler, "observation_archive"):
@@ -180,23 +188,38 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
180
188
  )
181
189
  log.info("Downloaded %d schedulable tasks(s).", len(tasks))
182
190
 
183
- # download projects
184
- self._projects = await self._task_archive.get_projects()
185
-
186
- # compare new and old lists
191
+ # download projects -- keep the old list around long enough to diff against below,
192
+ # mirroring self._tasks (only overwritten once _need_update has been decided)
193
+ old_projects = self._projects
194
+ projects = await self._task_archive.get_projects()
195
+ # `updated_at` is excluded: it's a portal-side save timestamp (round-trip-only field, not
196
+ # scheduling-relevant), and a no-op re-save still bumps it -- comparing it in would force
197
+ # a reschedule on every no-op edit, not just real content changes.
198
+ projects_changed = {p.code: p.model_dump(exclude={"updated_at"}) for p in projects} != {
199
+ p.code: p.model_dump(exclude={"updated_at"}) for p in old_projects
200
+ }
201
+
202
+ # compare new and old lists, both by ID (removed/added) and by content (changed, same ID)
187
203
  removed, added = self._compare_task_lists(self._tasks, tasks)
204
+ changed = self._changed_task_ids(self._tasks, tasks)
188
205
 
189
206
  # schedule update
190
207
  self._need_update = True
191
208
 
192
- # no changes?
193
- if not self._trigger_on_every_update and len(removed) == 0 and len(added) == 0:
209
+ # no changes at all?
210
+ if (
211
+ not self._trigger_on_every_update
212
+ and len(removed) == 0
213
+ and len(added) == 0
214
+ and len(changed) == 0
215
+ and not projects_changed
216
+ ):
194
217
  # no need to re-schedule
195
218
  log.info("No change in list of blocks detected.")
196
219
  self._need_update = False
197
220
 
198
- # has only the current block been removed?
199
- log.info("Removed: %d, added: %d", len(removed), len(added))
221
+ # has only the current block been removed, with nothing else changed?
222
+ log.info("Removed: %d, added: %d, changed: %d", len(removed), len(added), len(changed))
200
223
  if len(removed) == 1:
201
224
  log.info(
202
225
  "Found 1 removed block with ID %d. Last task ID was %s, current is %s.",
@@ -204,21 +227,20 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
204
227
  str(self._last_task_id),
205
228
  str(self._current_task_id),
206
229
  )
207
- if len(removed) == 1 and len(added) == 0 and removed[0] == self._last_task_id:
230
+ if (
231
+ len(removed) == 1
232
+ and len(added) == 0
233
+ and len(changed) == 0
234
+ and not projects_changed
235
+ and removed[0] == self._last_task_id
236
+ ):
208
237
  # no need to re-schedule
209
238
  log.info("Only one removed block detected, which is the one currently running.")
210
239
  self._need_update = False
211
240
 
212
- # check, if one of the removed blocks was actually in schedule
213
- if len(removed) > 0 and self._need_update:
214
- schedule = await self._schedule.get_schedule()
215
- removed_from_schedule = [s for s in schedule if s.task.id in removed]
216
- if len(removed_from_schedule) == 0:
217
- log.info("Found %s tasks, but none of them was scheduled.", len(removed))
218
- self._need_update = False
219
-
220
241
  # store blocks
221
242
  self._tasks = tasks
243
+ self._projects = projects
222
244
 
223
245
  # schedule update
224
246
  if self._need_update:
@@ -252,6 +274,33 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
252
274
 
253
275
  return sorted(additional1), sorted(additional2) # type: ignore[type-var]
254
276
 
277
+ @staticmethod
278
+ def _content_dump(task: Task) -> dict[str, Any]:
279
+ """``task.model_dump(exclude={"updated_at"})``.
280
+
281
+ ``updated_at`` is a portal-side save timestamp, not scheduling-relevant content, and a
282
+ no-op re-save (e.g. an unchanged DRF PATCH) still bumps it -- leaving it in would report
283
+ every no-op edit as a "changed" task.
284
+ """
285
+ return task.model_dump(exclude={"updated_at"})
286
+
287
+ @classmethod
288
+ def _changed_task_ids(cls, tasks1: list[Task], tasks2: list[Task]) -> set[Any]:
289
+ """IDs present in both lists whose task content differs between them.
290
+
291
+ Comparison is via ``model_dump()`` (see :meth:`_content_dump`), not pydantic ``==``:
292
+ ``Task.__eq__`` also compares runtime-only ``PrivateAttr``s (e.g. ``_cant_run_reason`` set
293
+ by ``can_run()``, or ``_resolved_target`` set while scheduling), which would make a task
294
+ that has merely been scheduled/run look "changed" on the next poll. ``model_dump()``
295
+ excludes private attributes by construction. What's left after also dropping
296
+ ``updated_at`` (priority, duration, script, ...) are the fields that actually matter for
297
+ scheduling.
298
+ """
299
+ by_id1 = {t.id: t for t in tasks1}
300
+ by_id2 = {t.id: t for t in tasks2}
301
+ common = set(by_id1).intersection(by_id2)
302
+ return {i for i in common if cls._content_dump(by_id1[i]) != cls._content_dump(by_id2[i])}
303
+
255
304
  async def _schedule_worker(self) -> None:
256
305
  await asyncio.sleep(5)
257
306
 
@@ -285,7 +334,10 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
285
334
  # schedule
286
335
  scheduled_tasks = ObservationList()
287
336
  first = True
288
- async for scheduled_task in self._scheduler.schedule(self._tasks, self._projects, start, end):
337
+ instrument_capabilities = self._task_archive.get_instrument_capabilities()
338
+ async for scheduled_task in self._scheduler.schedule(
339
+ self._tasks, self._projects, start, end, instrument_capabilities=instrument_capabilities
340
+ ):
289
341
  # remember for later
290
342
  scheduled_tasks.append(scheduled_task)
291
343
 
@@ -122,7 +122,7 @@ class DummyRoof(BaseRoof, IRoof):
122
122
  await self._change_motion_status(MotionStatus.ABORTING)
123
123
 
124
124
  async with LockWithAbort(self._lock_motion, self._abort_motion):
125
- await self._change_motion_status(MotionStatus.IDLE)
125
+ await self._change_motion_status(MotionStatus.PARKED if self._is_closed() else MotionStatus.IDLE)
126
126
 
127
127
 
128
128
  __all__ = ["DummyRoof"]
@@ -78,9 +78,12 @@ class MissingObserverError(exc.MotionError):
78
78
  pass
79
79
 
80
80
 
81
- class AltitudeLimitError(exc.MotionError):
81
+ class AltitudeLimitError(exc.PyobsError):
82
82
  """Destination altitude is below the configured limit -- an expected, deferrable condition
83
- from a scheduler's point of view, not a failure to alert on."""
83
+ from a scheduler's point of view, not a failure to alert on. Deliberately not a MotionError:
84
+ it's not a motion fault (nothing was attempted), so it must not count towards the module's
85
+ repeated-motion-failure escalation (see _register_exception(exc.MotionError, ...) below) --
86
+ picking a target below the horizon is normal, not a sign the telescope is broken."""
84
87
 
85
88
  pass
86
89