pyobs-core 2.2.0__tar.gz → 2.3.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 (456) hide show
  1. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/PKG-INFO +1 -1
  2. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/dummycamera.py +21 -2
  3. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/focus/focusseries.py +2 -2
  4. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/image/imagewatcher.py +61 -25
  5. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/module.py +2 -2
  6. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/scheduler.py +58 -17
  7. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/roof/dummyroof.py +1 -1
  8. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/telescope/basetelescope.py +5 -2
  9. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/filesystem/observationarchive.py +6 -0
  10. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/portal/observationarchive.py +38 -5
  11. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/portal/taskarchive.py +8 -4
  12. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/task.py +4 -0
  13. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/pipeline/reduction.py +9 -4
  14. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/serialization.py +49 -3
  15. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyproject.toml +1 -1
  16. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/.gitignore +0 -0
  17. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/LICENSE +0 -0
  18. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/__init__.py +0 -0
  19. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/application.py +0 -0
  20. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/background_task.py +0 -0
  21. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/cli/__init__.py +0 -0
  22. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/cli/_cli.py +0 -0
  23. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/cli/pyobs.py +0 -0
  24. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/cli/pyobsd.py +0 -0
  25. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/cli/pyobsw.py +0 -0
  26. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/__init__.py +0 -0
  27. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/comm.py +0 -0
  28. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/commlogging.py +0 -0
  29. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/dummy/__init__.py +0 -0
  30. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/dummy/dummycomm.py +0 -0
  31. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/local/__init__.py +0 -0
  32. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/local/localcomm.py +0 -0
  33. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/local/localnetwork.py +0 -0
  34. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/proxy.py +0 -0
  35. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/__init__.py +0 -0
  36. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/rpc.py +0 -0
  37. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/serializer.py +0 -0
  38. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009/__init__.py +0 -0
  39. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009/binding.py +0 -0
  40. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009/rpc.py +0 -0
  41. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009_timeout/__init__.py +0 -0
  42. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009_timeout/rpc.py +0 -0
  43. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009_timeout/stanza/RPC.py +0 -0
  44. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xep_0009_timeout/stanza/__init__.py +0 -0
  45. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xmppclient.py +0 -0
  46. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/comm/xmpp/xmppcomm.py +0 -0
  47. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/__init__.py +0 -0
  48. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/badweather.py +0 -0
  49. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/event.py +0 -0
  50. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/exposurestatuschanged.py +0 -0
  51. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/filterchanged.py +0 -0
  52. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/focusfound.py +0 -0
  53. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/goodweather.py +0 -0
  54. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/log.py +0 -0
  55. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/modechanged.py +0 -0
  56. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/moduleclosed.py +0 -0
  57. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/moduleopened.py +0 -0
  58. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/motionstatuschanged.py +0 -0
  59. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/move.py +0 -0
  60. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/newimage.py +0 -0
  61. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/newspectrum.py +0 -0
  62. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/offsets.py +0 -0
  63. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/roofclosing.py +0 -0
  64. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/roofopened.py +0 -0
  65. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/taskfailed.py +0 -0
  66. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/taskfinished.py +0 -0
  67. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/taskstarted.py +0 -0
  68. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/events/testevent.py +0 -0
  69. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/__init__.py +0 -0
  70. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/image.py +0 -0
  71. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/__init__.py +0 -0
  72. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/altazoffsets.py +0 -0
  73. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/exptime.py +0 -0
  74. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/genericoffset.py +0 -0
  75. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/onskydistance.py +0 -0
  76. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/pixeloffsets.py +0 -0
  77. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/radecoffsets.py +0 -0
  78. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/meta/skyoffsets.py +0 -0
  79. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processor.py +0 -0
  80. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/__init__.py +0 -0
  81. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/_daobackgroundremover.py +0 -0
  82. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/annotation/__init__.py +0 -0
  83. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/annotation/_pillow.py +0 -0
  84. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/annotation/circle.py +0 -0
  85. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/annotation/crosshair.py +0 -0
  86. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/annotation/text.py +0 -0
  87. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/__init__.py +0 -0
  88. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/_dotnet_request.py +0 -0
  89. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/_dotnet_request_builder.py +0 -0
  90. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/_dotnet_request_logger.py +0 -0
  91. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/_dotnet_response_saver.py +0 -0
  92. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/astrometry.py +0 -0
  93. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/astrometry/dotnet.py +0 -0
  94. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/calibration/__init__.py +0 -0
  95. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/calibration/_calibration_cache.py +0 -0
  96. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/calibration/_ccddata_calibrator.py +0 -0
  97. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/calibration/calibration.py +0 -0
  98. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/__init__.py +0 -0
  99. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/_pysep_stats_calculator.py +0 -0
  100. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/_source_catalog.py +0 -0
  101. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/daophot.py +0 -0
  102. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/pysep.py +0 -0
  103. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/simpledisk.py +0 -0
  104. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/detection/sourcedetection.py +0 -0
  105. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/exptime/__init__.py +0 -0
  106. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/exptime/exptime.py +0 -0
  107. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/exptime/star.py +0 -0
  108. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/__init__.py +0 -0
  109. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/addfitsheaders.py +0 -0
  110. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/download.py +0 -0
  111. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/flip.py +0 -0
  112. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/grayscale.py +0 -0
  113. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/httpserver.py +0 -0
  114. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/normalize.py +0 -0
  115. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/save.py +0 -0
  116. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/saveimage.py +0 -0
  117. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/smooth.py +0 -0
  118. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/image/softbin.py +0 -0
  119. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/__init__.py +0 -0
  120. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/addmask.py +0 -0
  121. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/catalogcircularmask.py +0 -0
  122. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/circularmask.py +0 -0
  123. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/createfilename.py +0 -0
  124. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/imagesourcefilter.py +0 -0
  125. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/misc/removebackground.py +0 -0
  126. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/modules/__init__.py +0 -0
  127. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/modules/getfitsheaders.py +0 -0
  128. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/__init__.py +0 -0
  129. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/astrometry.py +0 -0
  130. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/brighteststar.py +0 -0
  131. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/brighteststar_guiding.py +0 -0
  132. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/dummyoffsets.py +0 -0
  133. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/dummyskyoffsets.py +0 -0
  134. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/fitsheader.py +0 -0
  135. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/offsets.py +0 -0
  136. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/pixeloffsets.py +0 -0
  137. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/projected.py +0 -0
  138. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/offsets/spilled_light.py +0 -0
  139. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/__init__.py +0 -0
  140. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/_photometry_calculator.py +0 -0
  141. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/_photutil_aperture_photometry.py +0 -0
  142. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/_sep_aperture_photometry.py +0 -0
  143. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/aperture_photometry.py +0 -0
  144. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/photometry.py +0 -0
  145. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/photutil.py +0 -0
  146. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/photometry/pysep.py +0 -0
  147. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/wcs/__init__.py +0 -0
  148. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/images/processors/wcs/solarhelioprojective.py +0 -0
  149. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IAbortable.py +0 -0
  150. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IAcquisition.py +0 -0
  151. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IAutoFocus.py +0 -0
  152. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IAutoGuiding.py +0 -0
  153. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IAutonomous.py +0 -0
  154. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IBinning.py +0 -0
  155. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ICalibrate.py +0 -0
  156. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ICamera.py +0 -0
  157. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IConfig.py +0 -0
  158. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ICooling.py +0 -0
  159. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IData.py +0 -0
  160. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IDataSequence.py +0 -0
  161. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IDome.py +0 -0
  162. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IExposure.py +0 -0
  163. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IExposureTime.py +0 -0
  164. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IFilters.py +0 -0
  165. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IFitsHeaderAfter.py +0 -0
  166. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IFitsHeaderBefore.py +0 -0
  167. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IFlatField.py +0 -0
  168. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IFocusModel.py +0 -0
  169. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IFocuser.py +0 -0
  170. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IGain.py +0 -0
  171. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IImageFormat.py +0 -0
  172. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IImageType.py +0 -0
  173. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IMode.py +0 -0
  174. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IModule.py +0 -0
  175. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IMotion.py +0 -0
  176. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IMultiFiber.py +0 -0
  177. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IOffsetsAltAz.py +0 -0
  178. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IOffsetsRaDec.py +0 -0
  179. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingAltAz.py +0 -0
  180. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingBody.py +0 -0
  181. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingHeliocentricPolar.py +0 -0
  182. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingHeliographicStonyhurst.py +0 -0
  183. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingHelioprojective.py +0 -0
  184. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingOrbitalElements.py +0 -0
  185. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingRaDec.py +0 -0
  186. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IPointingSeries.py +0 -0
  187. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IReady.py +0 -0
  188. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IRobotic.py +0 -0
  189. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IRoboticScheduler.py +0 -0
  190. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IRoof.py +0 -0
  191. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IRotation.py +0 -0
  192. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IRunnable.py +0 -0
  193. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IRunning.py +0 -0
  194. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IScriptRunner.py +0 -0
  195. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ISpectrograph.py +0 -0
  196. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IStartStop.py +0 -0
  197. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IStructuredConfig.py +0 -0
  198. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ISyncTarget.py +0 -0
  199. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ITelescope.py +0 -0
  200. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ITemperatures.py +0 -0
  201. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ITrackingMode.py +0 -0
  202. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/ITrackingRate.py +0 -0
  203. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IVideo.py +0 -0
  204. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IWeather.py +0 -0
  205. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/IWindow.py +0 -0
  206. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/__init__.py +0 -0
  207. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/interfaces/interface.py +0 -0
  208. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/__init__.py +0 -0
  209. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/camerasettings.py +0 -0
  210. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/fitsheader.py +0 -0
  211. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/fitsnamespace.py +0 -0
  212. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/follow.py +0 -0
  213. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/motionstatus.py +0 -0
  214. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/pipeline.py +0 -0
  215. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/waitformotion.py +0 -0
  216. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/mixins/weatheraware.py +0 -0
  217. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/__init__.py +0 -0
  218. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/__init__.py +0 -0
  219. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/adaptive.py +0 -0
  220. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/basecamera.py +0 -0
  221. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/basespectrograph.py +0 -0
  222. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/basevideo.py +0 -0
  223. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/dummyspectrograph.py +0 -0
  224. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/dummyvideo.py +0 -0
  225. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/camera/pipelinecamera.py +0 -0
  226. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/flatfield/__init__.py +0 -0
  227. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/flatfield/flatfield.py +0 -0
  228. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/flatfield/pointing.py +0 -0
  229. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/flatfield/scheduler.py +0 -0
  230. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/focus/__init__.py +0 -0
  231. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/focus/dummyautofocus.py +0 -0
  232. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/focus/focusmodel.py +0 -0
  233. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/image/__init__.py +0 -0
  234. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/image/imagewriter.py +0 -0
  235. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/image/pipeline.py +0 -0
  236. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/image/seeing.py +0 -0
  237. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/__init__.py +0 -0
  238. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/_base.py +0 -0
  239. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/_baseguiding.py +0 -0
  240. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/acquisition.py +0 -0
  241. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/autoguiding.py +0 -0
  242. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/dummyacquisition.py +0 -0
  243. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/dummyguiding.py +0 -0
  244. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/guidingstatistics/__init__.py +0 -0
  245. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/guidingstatistics/guidingstatistics.py +0 -0
  246. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/guidingstatistics/pixeloffset.py +0 -0
  247. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/guidingstatistics/skyoffsets.py +0 -0
  248. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/guidingstatistics/uptime.py +0 -0
  249. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/pointing/scienceframeguiding.py +0 -0
  250. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/__init__.py +0 -0
  251. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/dummymastermind.py +0 -0
  252. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/dummyscheduler.py +0 -0
  253. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/mastermind.py +0 -0
  254. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/pointing.py +0 -0
  255. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/robotic/scriptrunner.py +0 -0
  256. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/roof/__init__.py +0 -0
  257. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/roof/basedome.py +0 -0
  258. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/roof/baseroof.py +0 -0
  259. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/telescope/__init__.py +0 -0
  260. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/telescope/_dummytelescopebase.py +0 -0
  261. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/telescope/dummyaltaztelescope.py +0 -0
  262. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/telescope/dummyradectelescope.py +0 -0
  263. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/telescope/dummysolartelescope.py +0 -0
  264. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/test/__init__.py +0 -0
  265. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/test/standalone.py +0 -0
  266. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/__init__.py +0 -0
  267. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/dummymode.py +0 -0
  268. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/dummystructuredconfig.py +0 -0
  269. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/fluentlogger.py +0 -0
  270. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/httpfilecache.py +0 -0
  271. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/kiosk.py +0 -0
  272. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/matrix.py +0 -0
  273. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/stellarium.py +0 -0
  274. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/telegram.py +0 -0
  275. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/utils/trigger.py +0 -0
  276. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/weather/__init__.py +0 -0
  277. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/weather/mockweather.py +0 -0
  278. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/weather/weather.py +0 -0
  279. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/weather/weather_api.py +0 -0
  280. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/modules/weather/weather_state.py +0 -0
  281. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/object.py +0 -0
  282. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/py.typed +0 -0
  283. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/__init__.py +0 -0
  284. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/observation.py +0 -0
  285. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/__init__.py +0 -0
  286. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/_executor.py +0 -0
  287. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/astroplanscheduler.py +0 -0
  288. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/__init__.py +0 -0
  289. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/airmassconstraint.py +0 -0
  290. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/constraint.py +0 -0
  291. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/moonilluminationconstraint.py +0 -0
  292. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/moonseparationconstraint.py +0 -0
  293. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/solarelevationconstraint.py +0 -0
  294. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/constraints/timeconstraint.py +0 -0
  295. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/dataprovider.py +0 -0
  296. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/__init__.py +0 -0
  297. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/aftertime.py +0 -0
  298. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/beforetime.py +0 -0
  299. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/constant.py +0 -0
  300. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/follow.py +0 -0
  301. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/interval.py +0 -0
  302. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/merit.py +0 -0
  303. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/pernight.py +0 -0
  304. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/random.py +0 -0
  305. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/timewindow.py +0 -0
  306. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/merits/transit.py +0 -0
  307. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/observationarchiveevolution.py +0 -0
  308. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/ondemandscheduler.py +0 -0
  309. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/__init__.py +0 -0
  310. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/dynamictarget.py +0 -0
  311. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/heliocentricpolartarget.py +0 -0
  312. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/helioprojectivetarget.py +0 -0
  313. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/picker/__init__.py +0 -0
  314. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/picker/csvpicker.py +0 -0
  315. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/picker/picker.py +0 -0
  316. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/siderealtarget.py +0 -0
  317. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/targets/target.py +0 -0
  318. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scheduler/taskscheduler.py +0 -0
  319. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/__init__.py +0 -0
  320. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/calibration/__init__.py +0 -0
  321. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/calibration/darkbias.py +0 -0
  322. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/calibration/pointing.py +0 -0
  323. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/calibration/skyflats.py +0 -0
  324. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/control/__init__.py +0 -0
  325. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/control/cases.py +0 -0
  326. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/control/conditional.py +0 -0
  327. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/control/parallel.py +0 -0
  328. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/control/selector.py +0 -0
  329. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/control/sequential.py +0 -0
  330. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/exceptions.py +0 -0
  331. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/imaging/__init__.py +0 -0
  332. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/imaging/autofocus.py +0 -0
  333. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/imaging/imaging.py +0 -0
  334. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/imaging/transitimaging.py +0 -0
  335. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/script.py +0 -0
  336. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/utils/__init__.py +0 -0
  337. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/utils/callmodule.py +0 -0
  338. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/utils/debugtrigger.py +0 -0
  339. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/scripts/utils/log.py +0 -0
  340. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/__init__.py +0 -0
  341. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/filesystem/__init__.py +0 -0
  342. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/filesystem/taskarchive.py +0 -0
  343. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/__init__.py +0 -0
  344. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/_portal.py +0 -0
  345. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/_schedulereader.py +0 -0
  346. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/_schedulewriter.py +0 -0
  347. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/configdb.py +0 -0
  348. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/mockobservationarchive.py +0 -0
  349. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/observationarchive.py +0 -0
  350. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/scripts/__init__.py +0 -0
  351. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/scripts/autofocus.py +0 -0
  352. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/scripts/default.py +0 -0
  353. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/scripts/script.py +0 -0
  354. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/task.py +0 -0
  355. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/taskarchive.py +0 -0
  356. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/lco/taskrunner.py +0 -0
  357. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/memory/__init__.py +0 -0
  358. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/memory/observationarchive.py +0 -0
  359. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/memory/taskarchive.py +0 -0
  360. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/observationarchive.py +0 -0
  361. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/portal/__init__.py +0 -0
  362. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/storage/taskarchive.py +0 -0
  363. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/taskrunner.py +0 -0
  364. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/__init__.py +0 -0
  365. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/archive/__init__.py +0 -0
  366. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/archive/archive.py +0 -0
  367. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/archive/local_archive.py +0 -0
  368. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/archive/pyobs_archive.py +0 -0
  369. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/calibration.py +0 -0
  370. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/exptime/__init__.py +0 -0
  371. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/exptime/exptime.py +0 -0
  372. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/exptime/stellarexptime.py +0 -0
  373. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/__init__.py +0 -0
  374. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/exptimeeval.py +0 -0
  375. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/flatfielder.py +0 -0
  376. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/pointing/__init__.py +0 -0
  377. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/pointing/base.py +0 -0
  378. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/pointing/static.py +0 -0
  379. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/priorities/__init__.py +0 -0
  380. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/priorities/archive.py +0 -0
  381. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/priorities/base.py +0 -0
  382. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/priorities/const.py +0 -0
  383. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/robotic/utils/skyflats/scheduler.py +0 -0
  384. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/__init__.py +0 -0
  385. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/average.py +0 -0
  386. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/cache.py +0 -0
  387. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/config.py +0 -0
  388. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/config_schema.py +0 -0
  389. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/coordinates.py +0 -0
  390. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/curvefit.py +0 -0
  391. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/enums.py +0 -0
  392. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/exceptions.py +0 -0
  393. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/exptime_grouping.py +0 -0
  394. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/fits.py +0 -0
  395. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/focusseries/__init__.py +0 -0
  396. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/focusseries/base.py +0 -0
  397. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/focusseries/photometry.py +0 -0
  398. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/focusseries/projection.py +0 -0
  399. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/grids/__init__.py +0 -0
  400. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/grids/filters.py +0 -0
  401. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/grids/grid.py +0 -0
  402. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/grids/gridnode.py +0 -0
  403. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/grids/pipeline.py +0 -0
  404. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/__init__.py +0 -0
  405. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/__init__.py +0 -0
  406. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/binningwidget.py +0 -0
  407. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/datadisplaywidget.py +0 -0
  408. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/exposewidget.py +0 -0
  409. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/exposuretimewidget.py +0 -0
  410. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/imageformatwidget.py +0 -0
  411. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/listpickerdialog.py +0 -0
  412. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/gui/camera/windowingwidget.py +0 -0
  413. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/http.py +0 -0
  414. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/influxdb.py +0 -0
  415. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/logger.py +0 -0
  416. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/logging/__init__.py +0 -0
  417. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/logging/context.py +0 -0
  418. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/logging/resolvableerror.py +0 -0
  419. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/modulegui.py +0 -0
  420. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/offsets/__init__.py +0 -0
  421. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/offsets/applyaltazoffsets.py +0 -0
  422. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/offsets/applyoffsets.py +0 -0
  423. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/offsets/applyradecoffsets.py +0 -0
  424. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/parallel.py +0 -0
  425. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/pipeline/__init__.py +0 -0
  426. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/pipeline/pipeline.py +0 -0
  427. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/pipeline/progress.py +0 -0
  428. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/pipeline/reduction_base.py +0 -0
  429. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/publisher/__init__.py +0 -0
  430. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/publisher/csv.py +0 -0
  431. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/publisher/http.py +0 -0
  432. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/publisher/log.py +0 -0
  433. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/publisher/multi.py +0 -0
  434. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/publisher/publisher.py +0 -0
  435. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/shellcommand.py +0 -0
  436. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/threads/__init__.py +0 -0
  437. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/threads/lockwithabort.py +0 -0
  438. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/time.py +0 -0
  439. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/units.py +0 -0
  440. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/utils/versions.py +0 -0
  441. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/version.py +0 -0
  442. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/__init__.py +0 -0
  443. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/archivefile.py +0 -0
  444. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/bufferedfile.py +0 -0
  445. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/file.py +0 -0
  446. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/filelists/__init__.py +0 -0
  447. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/filelists/filelist.py +0 -0
  448. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/filelists/testing.py +0 -0
  449. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/httpfile.py +0 -0
  450. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/localfile.py +0 -0
  451. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/memfile.py +0 -0
  452. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/sftpfile.py +0 -0
  453. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/smbfile.py +0 -0
  454. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/sshfile.py +0 -0
  455. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/tempfile.py +0 -0
  456. {pyobs_core-2.2.0 → pyobs_core-2.3.0}/pyobs/vfs/vfs.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.3.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
@@ -47,6 +47,7 @@ class ImageWatcher(Module):
47
47
  poll_interval: int = 5,
48
48
  wait_time: int = 10,
49
49
  pattern: str = "*",
50
+ flatten: bool = True,
50
51
  **kwargs: Any,
51
52
  ):
52
53
  """Create a new image watcher.
@@ -59,6 +60,26 @@ class ImageWatcher(Module):
59
60
  wait_time: Time in seconds between adding a file to the list and processing it. Gives a file that is
60
61
  still being written time to finish (relevant for poll mode and the initial scan) and spaces out
61
62
  re-queued files after failed destination copies.
63
+ pattern: Only watch/process files matching this fnmatch pattern, checked against the full path (so
64
+ e.g. "*.fits" also matches files found in subdirectories). This is the single point of filtering,
65
+ applied uniformly in ``add_file`` regardless of watch mode -- inotify events, polling, and the
66
+ initial scan in ``open()`` all discover candidates first and let ``add_file`` decide, none of
67
+ them pre-filter by pattern before that.
68
+ flatten: For a non-templated destination (one without ``{placeholder}``s), whether to collapse the
69
+ file's path to just its basename under that destination (the historical behavior, default) or to
70
+ preserve its path relative to ``watchpath`` under the destination instead. Files found via
71
+ recursive watching or polling only keep their subdirectory structure at the destination when this
72
+ is False. Note this relies on the destination VFS root creating missing parent directories on
73
+ write (the default for e.g. ``LocalFile``) -- a destination configured to not do that would
74
+ repeatedly fail and re-queue any file whose relative path needs a parent directory that doesn't
75
+ exist yet.
76
+
77
+ Note:
78
+ If a directory holding a file that's already queued (added but not yet processed by ``_worker``) is
79
+ renamed before that happens, the worker's read of the file at its captured (pre-rename) path fails
80
+ and the file is dropped rather than picked up at its new location -- this is unrelated to, and not
81
+ covered by, the recursive-watching support for directories renamed after they no longer hold queued
82
+ files. Avoid renaming directories that may still hold queued/retrying files.
62
83
  """
63
84
  Module.__init__(self, **kwargs)
64
85
 
@@ -77,6 +98,7 @@ class ImageWatcher(Module):
77
98
  self._poll_interval = poll_interval
78
99
  self._wait_time = wait_time
79
100
  self._pattern = pattern
101
+ self._flatten = flatten
80
102
  self.current_file: CurrentFile | None = None
81
103
 
82
104
  # filename patterns
@@ -85,49 +107,58 @@ class ImageWatcher(Module):
85
107
  self._destinations = destinations
86
108
 
87
109
  async def _watch_inotify(self) -> None:
88
- from asyncinotify import Inotify, Mask
110
+ from asyncinotify import Mask, RecursiveWatcher
89
111
 
90
112
  # get local directory
91
113
  local = await self.vfs.local_path(self._watchpath)
92
114
 
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)
115
+ # recursively watch the local directory: watches are added for subdirectories as they're
116
+ # created or moved in, and removed as they're moved out (deletion needs no explicit
117
+ # removal -- the kernel invalidates the watch on its own, see the plan doc for details).
118
+ # A directory can also vanish between an event being received and asyncinotify acting on
119
+ # it (its own add_watch/rm_watch racing a concurrent delete), which raises OSError from
120
+ # inside the library rather than being handled there. Restart with a fresh watcher (a
121
+ # full re-walk) on that instead of letting it kill this task outright -- self-heals faster
122
+ # than falling back to the module's outer background-task restart-with-backoff.
123
+ while True:
124
+ try:
125
+ watcher = RecursiveWatcher(Path(local), Mask.CLOSE_WRITE)
126
+ async for event in watcher.watch_recursive():
127
+ # get filename by replacing local with watchpath
128
+ filename = str(event.path).replace(local, self._watchpath)
102
129
 
103
- # add file
104
- await self.add_file(filename)
130
+ # add file
131
+ await self.add_file(filename)
132
+ except OSError as e:
133
+ log.warning("Inotify watch error (directory likely vanished mid-scan), restarting watch: %s", e)
105
134
 
106
135
  async def _watch_poll(self) -> None:
107
- # init list
108
- files = set(await self.vfs.listdir(self._watchpath))
136
+ # init list (recursive, so subdirectories are picked up too; unfiltered by pattern here --
137
+ # add_file is the single point of pattern filtering, same as inotify mode)
138
+ files = set(await self.vfs.find(self._watchpath, "*"))
109
139
 
110
140
  # run forever
111
- path = PurePosixPath(self._watchpath)
112
141
  while True:
142
+ await asyncio.sleep(self._poll_interval)
143
+
113
144
  # get new list
114
- new_files = await self.vfs.listdir(self._watchpath)
145
+ new_files = set(await self.vfs.find(self._watchpath, "*"))
115
146
 
116
147
  # 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))
148
+ for f in new_files - files:
149
+ await self.add_file(os.path.join(self._watchpath, f))
121
150
 
122
151
  # store new list
123
- files = set(new_files)
152
+ files = new_files
124
153
 
125
154
  async def open(self) -> None:
126
155
  """Open image watcher."""
127
156
  await Module.open(self)
128
157
 
129
- # add all files from directory to queue
130
- for filename in await self.vfs.listdir(self._watchpath):
158
+ # add all files from directory to queue (recursive, so subdirectories are picked up too;
159
+ # unfiltered by pattern here -- add_file is the single point of pattern filtering, same as
160
+ # inotify mode)
161
+ for filename in await self.vfs.find(self._watchpath, "*"):
131
162
  await self.add_file(os.path.join(self._watchpath, filename))
132
163
 
133
164
  async def close(self) -> None:
@@ -193,10 +224,15 @@ class ImageWatcher(Module):
193
224
  if out_filename is None:
194
225
  raise ValueError("Could not create name for file.")
195
226
 
196
- else:
227
+ elif self._flatten:
197
228
  # no formatting, so just add filename to destination
198
229
  out_filename = os.path.join(pattern, os.path.basename(filename))
199
230
 
231
+ else:
232
+ # preserve the file's path relative to watchpath under the destination
233
+ rel = PurePosixPath(filename).relative_to(self._watchpath)
234
+ out_filename = str(PurePosixPath(pattern) / rel)
235
+
200
236
  # store it
201
237
  log.info("Storing file as %s...", out_filename)
202
238
  self.current_file.out_filename = out_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
@@ -180,23 +180,38 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
180
180
  )
181
181
  log.info("Downloaded %d schedulable tasks(s).", len(tasks))
182
182
 
183
- # download projects
184
- self._projects = await self._task_archive.get_projects()
185
-
186
- # compare new and old lists
183
+ # download projects -- keep the old list around long enough to diff against below,
184
+ # mirroring self._tasks (only overwritten once _need_update has been decided)
185
+ old_projects = self._projects
186
+ projects = await self._task_archive.get_projects()
187
+ # `updated_at` is excluded: it's a portal-side save timestamp (round-trip-only field, not
188
+ # scheduling-relevant), and a no-op re-save still bumps it -- comparing it in would force
189
+ # a reschedule on every no-op edit, not just real content changes.
190
+ projects_changed = {p.code: p.model_dump(exclude={"updated_at"}) for p in projects} != {
191
+ p.code: p.model_dump(exclude={"updated_at"}) for p in old_projects
192
+ }
193
+
194
+ # compare new and old lists, both by ID (removed/added) and by content (changed, same ID)
187
195
  removed, added = self._compare_task_lists(self._tasks, tasks)
196
+ changed = self._changed_task_ids(self._tasks, tasks)
188
197
 
189
198
  # schedule update
190
199
  self._need_update = True
191
200
 
192
- # no changes?
193
- if not self._trigger_on_every_update and len(removed) == 0 and len(added) == 0:
201
+ # no changes at all?
202
+ if (
203
+ not self._trigger_on_every_update
204
+ and len(removed) == 0
205
+ and len(added) == 0
206
+ and len(changed) == 0
207
+ and not projects_changed
208
+ ):
194
209
  # no need to re-schedule
195
210
  log.info("No change in list of blocks detected.")
196
211
  self._need_update = False
197
212
 
198
- # has only the current block been removed?
199
- log.info("Removed: %d, added: %d", len(removed), len(added))
213
+ # has only the current block been removed, with nothing else changed?
214
+ log.info("Removed: %d, added: %d, changed: %d", len(removed), len(added), len(changed))
200
215
  if len(removed) == 1:
201
216
  log.info(
202
217
  "Found 1 removed block with ID %d. Last task ID was %s, current is %s.",
@@ -204,21 +219,20 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
204
219
  str(self._last_task_id),
205
220
  str(self._current_task_id),
206
221
  )
207
- if len(removed) == 1 and len(added) == 0 and removed[0] == self._last_task_id:
222
+ if (
223
+ len(removed) == 1
224
+ and len(added) == 0
225
+ and len(changed) == 0
226
+ and not projects_changed
227
+ and removed[0] == self._last_task_id
228
+ ):
208
229
  # no need to re-schedule
209
230
  log.info("Only one removed block detected, which is the one currently running.")
210
231
  self._need_update = False
211
232
 
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
233
  # store blocks
221
234
  self._tasks = tasks
235
+ self._projects = projects
222
236
 
223
237
  # schedule update
224
238
  if self._need_update:
@@ -252,6 +266,33 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
252
266
 
253
267
  return sorted(additional1), sorted(additional2) # type: ignore[type-var]
254
268
 
269
+ @staticmethod
270
+ def _content_dump(task: Task) -> dict[str, Any]:
271
+ """``task.model_dump(exclude={"updated_at"})``.
272
+
273
+ ``updated_at`` is a portal-side save timestamp, not scheduling-relevant content, and a
274
+ no-op re-save (e.g. an unchanged DRF PATCH) still bumps it -- leaving it in would report
275
+ every no-op edit as a "changed" task.
276
+ """
277
+ return task.model_dump(exclude={"updated_at"})
278
+
279
+ @classmethod
280
+ def _changed_task_ids(cls, tasks1: list[Task], tasks2: list[Task]) -> set[Any]:
281
+ """IDs present in both lists whose task content differs between them.
282
+
283
+ Comparison is via ``model_dump()`` (see :meth:`_content_dump`), not pydantic ``==``:
284
+ ``Task.__eq__`` also compares runtime-only ``PrivateAttr``s (e.g. ``_cant_run_reason`` set
285
+ by ``can_run()``, or ``_resolved_target`` set while scheduling), which would make a task
286
+ that has merely been scheduled/run look "changed" on the next poll. ``model_dump()``
287
+ excludes private attributes by construction. What's left after also dropping
288
+ ``updated_at`` (priority, duration, script, ...) are the fields that actually matter for
289
+ scheduling.
290
+ """
291
+ by_id1 = {t.id: t for t in tasks1}
292
+ by_id2 = {t.id: t for t in tasks2}
293
+ common = set(by_id1).intersection(by_id2)
294
+ return {i for i in common if cls._content_dump(by_id1[i]) != cls._content_dump(by_id2[i])}
295
+
255
296
  async def _schedule_worker(self) -> None:
256
297
  await asyncio.sleep(5)
257
298
 
@@ -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
 
@@ -248,6 +248,12 @@ class YamlObservationArchive(FileSystemObservationArchive):
248
248
  return ObservationList([self.pyobs_model_validate(Observation, obs) for obs in observations])
249
249
 
250
250
  async def _save_observations_to_file(self, path: str, observations: ObservationList) -> None:
251
+ # exclude_defaults now also applies to the nested (PolymorphicBaseModel) Task/Constraint/
252
+ # etc. fields, not just Observation's own -- pyobs-core#855 fixed inject_class_on_
253
+ # serialization() silently ignoring it. Saved YAML files drop more default-valued task
254
+ # fields (active, empty constraints/merits/script, priority: 1.0, ...) than before; this
255
+ # is intentional and round-trips fine (model_validate refills the defaults), but anyone
256
+ # reading these files raw will notice the shape change.
251
257
  data = [obs.model_dump(mode="json", exclude_defaults=True) for obs in observations]
252
258
  with open(path, "w") as f:
253
259
  yaml.safe_dump(data, f)
@@ -37,6 +37,9 @@ class PortalObservationArchive(ObservationArchive):
37
37
  # First failure logs immediately at ERROR (someone should know right away); repeats
38
38
  # during the same outage are throttled to at most one ERROR per minute.
39
39
  self._poll_error_throttle = LogThrottle(quiet_for=0.0, interval=60.0)
40
+ # Same pattern, keyed per observation id: a portal outage would otherwise re-log an
41
+ # ERROR every poll for as long as the cancel PUT keeps failing.
42
+ self._cancel_error_throttle = LogThrottle(quiet_for=0.0, interval=60.0)
40
43
 
41
44
  if auto_update:
42
45
  self.add_background_task(self._check_for_changes)
@@ -171,6 +174,39 @@ class PortalObservationArchive(ObservationArchive):
171
174
  """
172
175
  return self._observations
173
176
 
177
+ async def _resolve_or_cancel_pending(self, obs: Observation, task_archive: TaskArchive) -> bool:
178
+ """Resolve a *pending* obs.task; if the task archive has completed at least one poll and
179
+ the portal no longer has the task, mark obs canceled so it stops being retried on every
180
+ subsequent poll instead of logged-and-skipped forever.
181
+
182
+ Only for pending observations: an in-progress one should be allowed to finish even if its
183
+ task was deactivated mid-run, not flipped to canceled out from under the mastermind.
184
+
185
+ Returns:
186
+ True if obs.task resolved and obs is usable.
187
+ """
188
+ task_id = obs.task
189
+ await obs.fetch_task(task_archive)
190
+ if obs.task is not None:
191
+ return True
192
+ if await task_archive.last_changed() is None:
193
+ # task archive has never completed a poll (e.g. still starting up) -- a genuinely
194
+ # missing task looks the same as this transient race, so don't cancel on it.
195
+ log.error("Could not resolve task for observation %s, skipping.", obs.id)
196
+ return False
197
+ log.error("Could not resolve task for observation %s, marking canceled.", obs.id)
198
+ obs.task = task_id # portal's task FK is non-nullable -- keep the id for the PUT payload
199
+ obs.state = ObservationState.CANCELED
200
+ try:
201
+ await self.update_observation(obs)
202
+ self._cancel_error_throttle.clear(str(obs.id))
203
+ except Exception as e:
204
+ if self._cancel_error_throttle.should_escalate(str(obs.id)):
205
+ log.error("Failed to mark observation %s canceled on portal: %s", obs.id, e)
206
+ else:
207
+ log.debug("Failed to mark observation %s canceled on portal: %s", obs.id, e)
208
+ return False
209
+
174
210
  async def get_next_observation(self, time: Time, task_archive: TaskArchive | None = None) -> Observation | None:
175
211
  """Returns the active scheduled task at the given time.
176
212
 
@@ -183,11 +219,8 @@ class PortalObservationArchive(ObservationArchive):
183
219
  """
184
220
  for obs in self._observations:
185
221
  if obs.state == "pending" and obs.start < time < obs.end:
186
- if task_archive is not None:
187
- await obs.fetch_task(task_archive)
188
- if obs.task is None:
189
- log.error("Could not resolve task for observation %s, skipping.", obs.id)
190
- continue
222
+ if task_archive is not None and not await self._resolve_or_cancel_pending(obs, task_archive):
223
+ continue
191
224
  return obs
192
225
  else:
193
226
  return None
@@ -92,13 +92,17 @@ class PortalTaskArchive(TaskArchive):
92
92
  ``model_dump()`` rather than pydantic ``==``, which also compares runtime attributes (e.g.
93
93
  ``Task._cant_run_reason`` set by ``can_run()``) and would flag unchanged tasks as changed
94
94
  on every poll; it is keyed by ID so that a stable reordering of the same items (e.g. an
95
- unordered portal queryset) is not mistaken for a change.
95
+ unordered portal queryset) is not mistaken for a change. ``updated_at`` is excluded from
96
+ both comparisons via ``exclude=`` so a no-op re-save doesn't trigger a re-download/
97
+ reschedule cascade (see pyobs-core#856).
96
98
  """
97
99
  projects = await self._get_projects()
98
100
  tasks = await self._get_tasks()
99
- if {p.code: p.model_dump() for p in projects} != {p.code: p.model_dump() for p in self._projects} or {
100
- t.id: t.model_dump() for t in tasks
101
- } != {t.id: t.model_dump() for t in self._tasks}:
101
+ if {p.code: p.model_dump(exclude={"updated_at"}) for p in projects} != {
102
+ p.code: p.model_dump(exclude={"updated_at"}) for p in self._projects
103
+ } or {t.id: t.model_dump(exclude={"updated_at"}) for t in tasks} != {
104
+ t.id: t.model_dump(exclude={"updated_at"}) for t in self._tasks
105
+ }:
102
106
  self._projects = projects
103
107
  self._tasks = tasks
104
108
  self._last_update = Time.now()
@@ -155,6 +155,10 @@ class Project(BaseModel):
155
155
  priority: float | None = Field(ge=0.0, le=9999.0, default=1.0)
156
156
  users: list[str] = Field(default_factory=list)
157
157
  public: bool = False # ingest backend `public` flag; default keeps old backends valid
158
+ updated_at: str | None = None
159
+ """Last-modification timestamp as emitted by pyobs-portal (DB-derived ``updated_at``,
160
+ added in pyobs-portal#134). Carried for round-trip fidelity so the strict ``Project`` model
161
+ (``extra="forbid"``) accepts the field the portal now sends; not used by pyobs itself."""
158
162
 
159
163
 
160
164
  __all__ = ["Task", "TaskData", "Project"]
@@ -44,6 +44,7 @@ class Reduction(ReductionBase):
44
44
  calib_science: bool = True,
45
45
  progress_callback: ProgressCallback | None = None,
46
46
  dark_exptime_tolerance: float = 0.01,
47
+ min_darks_per_group: int = 3,
47
48
  ):
48
49
  """Creates a Reduction object for reducing a given observation period.
49
50
 
@@ -60,6 +61,8 @@ class Reduction(ReductionBase):
60
61
  progress_callback: See ReductionBase.
61
62
  dark_exptime_tolerance: Relative tolerance for grouping a night's raw DARK frames
62
63
  into per-exptime master series -- see pyobs.utils.exptime_grouping.
64
+ min_darks_per_group: Minimum number of raw frames in a per-exptime DARK group to
65
+ combine a master dark from; smaller groups are skipped individually.
63
66
  """
64
67
  super().__init__(archive=archive, pipeline=pipeline, min_flats=min_flats, progress_callback=progress_callback)
65
68
 
@@ -70,6 +73,7 @@ class Reduction(ReductionBase):
70
73
  self._create_calibs = create_calibs
71
74
  self._calib_science = calib_science
72
75
  self._dark_exptime_tolerance = dark_exptime_tolerance
76
+ self._min_darks_per_group = min_darks_per_group
73
77
 
74
78
  # make sure the local output directory exists
75
79
  if self._store_local:
@@ -195,8 +199,9 @@ class Reduction(ReductionBase):
195
199
  """Creates one master dark per exposure time present in the night's raw DARK frames
196
200
  (grouped within self._dark_exptime_tolerance) -- a night can have darks at more than
197
201
  one exptime now that DarkBiasScript can match them to the night's science exptimes
198
- (#831). Frame groups with fewer than 3 members are skipped individually with a
199
- warning, rather than aborting dark reduction for this instrument/binning entirely.
202
+ (#831). Frame groups with fewer than self._min_darks_per_group members are skipped
203
+ individually with a warning, rather than aborting dark reduction for this
204
+ instrument/binning entirely.
200
205
 
201
206
  If none of the night's raw darks carry EXPTIME at all (a fully legacy instrument, as
202
207
  opposed to a handful of untagged frames mixed in with tagged ones), grouping would
@@ -247,12 +252,12 @@ class Reduction(ReductionBase):
247
252
  for group, exptime in groups:
248
253
  label = "legacy (no exptime)" if exptime is None else f"~{exptime}s"
249
254
 
250
- if len(group) < 3:
255
+ if len(group) < self._min_darks_per_group:
251
256
  log.warning("Too few (%d) dark frames at %s, skipping...", len(group), label)
252
257
  continue
253
258
 
254
259
  images = await self._archive.download_frames(group)
255
- if len(images) < 3:
260
+ if len(images) < self._min_darks_per_group:
256
261
  log.warning("Too few (%d) dark frames at %s, skipping...", len(images), label)
257
262
  continue
258
263
 
@@ -7,7 +7,7 @@ from typing import Any, Self, TypeVar
7
7
  from astroplan import Observer
8
8
  from pydantic import BaseModel as PydanticBaseModel
9
9
  from pydantic import ConfigDict, PrivateAttr, model_serializer, model_validator
10
- from pydantic_core.core_schema import ValidationInfo, ValidatorFunctionWrapHandler
10
+ from pydantic_core.core_schema import SerializationInfo, ValidationInfo, ValidatorFunctionWrapHandler
11
11
 
12
12
  from pyobs.comm import Comm
13
13
  from pyobs.object import PrivateAttrMixin
@@ -40,11 +40,57 @@ class BaseModel(PydanticBaseModel, PrivateAttrMixin):
40
40
  class PolymorphicBaseModel(BaseModel, metaclass=ABCMeta): # type: ignore[misc]
41
41
  """Pydantic base model for pyobs sub classes that need to be serialized."""
42
42
 
43
+ def _flat_field_spec(self, spec: Any, info_kwarg: str) -> set[str] | None:
44
+ """Reduce a pydantic `exclude=`/`include=` spec to a flat set of field names.
45
+
46
+ `inject_class_on_serialization` hand-builds its output dict instead of delegating to
47
+ `handler`, so it also has to hand-apply `exclude`/`include` itself -- pydantic's spec
48
+ grammar supports nested dicts (partial excludes on sub-fields) and integer/`__all__` keys
49
+ (per-element excludes on sequences), forwarded into each field's own serialization. Doing
50
+ that generically would mean reimplementing a chunk of pydantic-core's own traversal; since
51
+ every current caller only ever passes a flat set of top-level field names (see
52
+ pyobs-core#855), only that flat form is supported. Anything else raises rather than
53
+ silently ignoring or partially applying the spec.
54
+ """
55
+ if spec is None:
56
+ return None
57
+ if isinstance(spec, set | frozenset):
58
+ return set(spec)
59
+ if isinstance(spec, dict) and all(v is True for v in spec.values()):
60
+ return set(spec.keys())
61
+ raise NotImplementedError(
62
+ f"PolymorphicBaseModel.model_dump() only supports a flat field-name {info_kwarg}= "
63
+ f"(no nested specs) -- got {spec!r} for {type(self).__name__}; see pyobs-core#855"
64
+ )
65
+
43
66
  @model_serializer(mode="wrap")
44
- def inject_class_on_serialization(self, handler: ValidatorFunctionWrapHandler) -> dict[str, Any]:
67
+ def inject_class_on_serialization(
68
+ self, handler: ValidatorFunctionWrapHandler, info: SerializationInfo
69
+ ) -> dict[str, Any]:
45
70
  # Collect fields from the concrete runtime type to avoid Pydantic v2
46
71
  # resolving field schemas against the abstract base type when nested in a parent model
47
- result = {field_name: getattr(self, field_name) for field_name in type(self).model_fields}
72
+ if info.exclude_computed_fields and type(self).__pydantic_decorators__.computed_fields:
73
+ raise NotImplementedError(
74
+ f"PolymorphicBaseModel.model_dump() doesn't support exclude_computed_fields= for "
75
+ f"{type(self).__name__}; see pyobs-core#855"
76
+ )
77
+ exclude = self._flat_field_spec(info.exclude, "exclude")
78
+ include = self._flat_field_spec(info.include, "include")
79
+ fields = type(self).model_fields
80
+ names = [n for n in fields if (include is None or n in include) and (exclude is None or n not in exclude)]
81
+
82
+ result: dict[str, Any] = {}
83
+ for name in names:
84
+ value = getattr(self, name)
85
+ if info.exclude_none and value is None:
86
+ continue
87
+ if info.exclude_unset and name not in self.model_fields_set:
88
+ continue
89
+ if info.exclude_defaults and value == fields[name].get_default(call_default_factory=True):
90
+ continue
91
+ alias = fields[name].alias
92
+ key = alias if (info.by_alias and alias) else name
93
+ result[key] = value
48
94
  result["class"] = f"{self.__module__}.{self.__class__.__name__}"
49
95
  return result
50
96
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-core"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  description = "robotic telescope software"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
File without changes