pyobs-core 2.3.0__tar.gz → 2.4.1__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.3.0 → pyobs_core-2.4.1}/PKG-INFO +1 -1
  2. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/imagewatcher.py +40 -7
  3. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/scheduler.py +18 -7
  4. pyobs_core-2.4.1/pyobs/robotic/instruments.py +195 -0
  5. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/astroplanscheduler.py +11 -1
  6. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/ondemandscheduler.py +97 -21
  7. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/taskscheduler.py +7 -1
  8. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/darkbias.py +12 -1
  9. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/pointing.py +4 -2
  10. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/autofocus.py +4 -2
  11. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/imaging.py +53 -4
  12. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/portal/observationarchive.py +12 -9
  13. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/portal/taskarchive.py +54 -0
  14. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/taskarchive.py +8 -0
  15. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/task.py +8 -2
  16. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/time.py +10 -3
  17. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/file.py +12 -0
  18. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/localfile.py +31 -0
  19. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/vfs.py +20 -1
  20. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyproject.toml +1 -1
  21. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/.gitignore +0 -0
  22. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/LICENSE +0 -0
  23. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/__init__.py +0 -0
  24. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/application.py +0 -0
  25. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/background_task.py +0 -0
  26. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/__init__.py +0 -0
  27. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/_cli.py +0 -0
  28. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/pyobs.py +0 -0
  29. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/pyobsd.py +0 -0
  30. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/pyobsw.py +0 -0
  31. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/__init__.py +0 -0
  32. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/comm.py +0 -0
  33. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/commlogging.py +0 -0
  34. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/dummy/__init__.py +0 -0
  35. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/dummy/dummycomm.py +0 -0
  36. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/local/__init__.py +0 -0
  37. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/local/localcomm.py +0 -0
  38. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/local/localnetwork.py +0 -0
  39. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/proxy.py +0 -0
  40. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/__init__.py +0 -0
  41. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/rpc.py +0 -0
  42. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/serializer.py +0 -0
  43. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009/__init__.py +0 -0
  44. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009/binding.py +0 -0
  45. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009/rpc.py +0 -0
  46. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/__init__.py +0 -0
  47. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/rpc.py +0 -0
  48. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/stanza/RPC.py +0 -0
  49. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/stanza/__init__.py +0 -0
  50. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xmppclient.py +0 -0
  51. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xmppcomm.py +0 -0
  52. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/__init__.py +0 -0
  53. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/badweather.py +0 -0
  54. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/event.py +0 -0
  55. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/exposurestatuschanged.py +0 -0
  56. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/filterchanged.py +0 -0
  57. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/focusfound.py +0 -0
  58. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/goodweather.py +0 -0
  59. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/log.py +0 -0
  60. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/modechanged.py +0 -0
  61. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/moduleclosed.py +0 -0
  62. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/moduleopened.py +0 -0
  63. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/motionstatuschanged.py +0 -0
  64. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/move.py +0 -0
  65. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/newimage.py +0 -0
  66. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/newspectrum.py +0 -0
  67. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/offsets.py +0 -0
  68. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/roofclosing.py +0 -0
  69. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/roofopened.py +0 -0
  70. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/taskfailed.py +0 -0
  71. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/taskfinished.py +0 -0
  72. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/taskstarted.py +0 -0
  73. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/testevent.py +0 -0
  74. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/__init__.py +0 -0
  75. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/image.py +0 -0
  76. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/__init__.py +0 -0
  77. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/altazoffsets.py +0 -0
  78. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/exptime.py +0 -0
  79. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/genericoffset.py +0 -0
  80. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/onskydistance.py +0 -0
  81. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/pixeloffsets.py +0 -0
  82. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/radecoffsets.py +0 -0
  83. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/skyoffsets.py +0 -0
  84. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processor.py +0 -0
  85. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/__init__.py +0 -0
  86. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/_daobackgroundremover.py +0 -0
  87. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/__init__.py +0 -0
  88. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/_pillow.py +0 -0
  89. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/circle.py +0 -0
  90. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/crosshair.py +0 -0
  91. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/text.py +0 -0
  92. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/__init__.py +0 -0
  93. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_request.py +0 -0
  94. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_request_builder.py +0 -0
  95. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_request_logger.py +0 -0
  96. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_response_saver.py +0 -0
  97. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/astrometry.py +0 -0
  98. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/dotnet.py +0 -0
  99. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/__init__.py +0 -0
  100. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/_calibration_cache.py +0 -0
  101. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/_ccddata_calibrator.py +0 -0
  102. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/calibration.py +0 -0
  103. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/__init__.py +0 -0
  104. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/_pysep_stats_calculator.py +0 -0
  105. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/_source_catalog.py +0 -0
  106. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/daophot.py +0 -0
  107. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/pysep.py +0 -0
  108. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/simpledisk.py +0 -0
  109. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/sourcedetection.py +0 -0
  110. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/exptime/__init__.py +0 -0
  111. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/exptime/exptime.py +0 -0
  112. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/exptime/star.py +0 -0
  113. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/__init__.py +0 -0
  114. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/addfitsheaders.py +0 -0
  115. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/download.py +0 -0
  116. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/flip.py +0 -0
  117. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/grayscale.py +0 -0
  118. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/httpserver.py +0 -0
  119. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/normalize.py +0 -0
  120. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/save.py +0 -0
  121. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/saveimage.py +0 -0
  122. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/smooth.py +0 -0
  123. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/softbin.py +0 -0
  124. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/__init__.py +0 -0
  125. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/addmask.py +0 -0
  126. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/catalogcircularmask.py +0 -0
  127. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/circularmask.py +0 -0
  128. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/createfilename.py +0 -0
  129. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/imagesourcefilter.py +0 -0
  130. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/removebackground.py +0 -0
  131. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/modules/__init__.py +0 -0
  132. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/modules/getfitsheaders.py +0 -0
  133. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/__init__.py +0 -0
  134. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/astrometry.py +0 -0
  135. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/brighteststar.py +0 -0
  136. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/brighteststar_guiding.py +0 -0
  137. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/dummyoffsets.py +0 -0
  138. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/dummyskyoffsets.py +0 -0
  139. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/fitsheader.py +0 -0
  140. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/offsets.py +0 -0
  141. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/pixeloffsets.py +0 -0
  142. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/projected.py +0 -0
  143. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/spilled_light.py +0 -0
  144. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/__init__.py +0 -0
  145. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/_photometry_calculator.py +0 -0
  146. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/_photutil_aperture_photometry.py +0 -0
  147. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/_sep_aperture_photometry.py +0 -0
  148. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/aperture_photometry.py +0 -0
  149. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/photometry.py +0 -0
  150. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/photutil.py +0 -0
  151. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/pysep.py +0 -0
  152. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/wcs/__init__.py +0 -0
  153. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/wcs/solarhelioprojective.py +0 -0
  154. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAbortable.py +0 -0
  155. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAcquisition.py +0 -0
  156. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAutoFocus.py +0 -0
  157. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAutoGuiding.py +0 -0
  158. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAutonomous.py +0 -0
  159. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IBinning.py +0 -0
  160. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ICalibrate.py +0 -0
  161. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ICamera.py +0 -0
  162. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IConfig.py +0 -0
  163. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ICooling.py +0 -0
  164. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IData.py +0 -0
  165. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IDataSequence.py +0 -0
  166. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IDome.py +0 -0
  167. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IExposure.py +0 -0
  168. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IExposureTime.py +0 -0
  169. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFilters.py +0 -0
  170. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFitsHeaderAfter.py +0 -0
  171. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFitsHeaderBefore.py +0 -0
  172. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFlatField.py +0 -0
  173. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFocusModel.py +0 -0
  174. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFocuser.py +0 -0
  175. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IGain.py +0 -0
  176. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IImageFormat.py +0 -0
  177. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IImageType.py +0 -0
  178. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IMode.py +0 -0
  179. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IModule.py +0 -0
  180. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IMotion.py +0 -0
  181. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IMultiFiber.py +0 -0
  182. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IOffsetsAltAz.py +0 -0
  183. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IOffsetsRaDec.py +0 -0
  184. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingAltAz.py +0 -0
  185. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingBody.py +0 -0
  186. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingHeliocentricPolar.py +0 -0
  187. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingHeliographicStonyhurst.py +0 -0
  188. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingHelioprojective.py +0 -0
  189. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingOrbitalElements.py +0 -0
  190. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingRaDec.py +0 -0
  191. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingSeries.py +0 -0
  192. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IReady.py +0 -0
  193. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRobotic.py +0 -0
  194. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRoboticScheduler.py +0 -0
  195. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRoof.py +0 -0
  196. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRotation.py +0 -0
  197. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRunnable.py +0 -0
  198. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRunning.py +0 -0
  199. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IScriptRunner.py +0 -0
  200. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ISpectrograph.py +0 -0
  201. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IStartStop.py +0 -0
  202. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IStructuredConfig.py +0 -0
  203. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ISyncTarget.py +0 -0
  204. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITelescope.py +0 -0
  205. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITemperatures.py +0 -0
  206. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITrackingMode.py +0 -0
  207. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITrackingRate.py +0 -0
  208. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IVideo.py +0 -0
  209. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IWeather.py +0 -0
  210. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IWindow.py +0 -0
  211. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/__init__.py +0 -0
  212. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/interface.py +0 -0
  213. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/__init__.py +0 -0
  214. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/camerasettings.py +0 -0
  215. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/fitsheader.py +0 -0
  216. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/fitsnamespace.py +0 -0
  217. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/follow.py +0 -0
  218. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/motionstatus.py +0 -0
  219. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/pipeline.py +0 -0
  220. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/waitformotion.py +0 -0
  221. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/weatheraware.py +0 -0
  222. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/__init__.py +0 -0
  223. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/__init__.py +0 -0
  224. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/adaptive.py +0 -0
  225. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/basecamera.py +0 -0
  226. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/basespectrograph.py +0 -0
  227. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/basevideo.py +0 -0
  228. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/dummycamera.py +0 -0
  229. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/dummyspectrograph.py +0 -0
  230. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/dummyvideo.py +0 -0
  231. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/pipelinecamera.py +0 -0
  232. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/__init__.py +0 -0
  233. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/flatfield.py +0 -0
  234. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/pointing.py +0 -0
  235. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/scheduler.py +0 -0
  236. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/__init__.py +0 -0
  237. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/dummyautofocus.py +0 -0
  238. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/focusmodel.py +0 -0
  239. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/focusseries.py +0 -0
  240. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/__init__.py +0 -0
  241. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/imagewriter.py +0 -0
  242. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/pipeline.py +0 -0
  243. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/seeing.py +0 -0
  244. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/module.py +0 -0
  245. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/__init__.py +0 -0
  246. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/_base.py +0 -0
  247. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/_baseguiding.py +0 -0
  248. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/acquisition.py +0 -0
  249. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/autoguiding.py +0 -0
  250. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/dummyacquisition.py +0 -0
  251. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/dummyguiding.py +0 -0
  252. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/__init__.py +0 -0
  253. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/guidingstatistics.py +0 -0
  254. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/pixeloffset.py +0 -0
  255. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/skyoffsets.py +0 -0
  256. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/uptime.py +0 -0
  257. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/scienceframeguiding.py +0 -0
  258. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/__init__.py +0 -0
  259. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/dummymastermind.py +0 -0
  260. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/dummyscheduler.py +0 -0
  261. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/mastermind.py +0 -0
  262. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/pointing.py +0 -0
  263. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/scriptrunner.py +0 -0
  264. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/__init__.py +0 -0
  265. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/basedome.py +0 -0
  266. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/baseroof.py +0 -0
  267. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/dummyroof.py +0 -0
  268. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/__init__.py +0 -0
  269. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/_dummytelescopebase.py +0 -0
  270. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/basetelescope.py +0 -0
  271. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/dummyaltaztelescope.py +0 -0
  272. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/dummyradectelescope.py +0 -0
  273. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/dummysolartelescope.py +0 -0
  274. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/test/__init__.py +0 -0
  275. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/test/standalone.py +0 -0
  276. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/__init__.py +0 -0
  277. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/dummymode.py +0 -0
  278. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/dummystructuredconfig.py +0 -0
  279. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/fluentlogger.py +0 -0
  280. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/httpfilecache.py +0 -0
  281. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/kiosk.py +0 -0
  282. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/matrix.py +0 -0
  283. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/stellarium.py +0 -0
  284. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/telegram.py +0 -0
  285. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/trigger.py +0 -0
  286. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/__init__.py +0 -0
  287. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/mockweather.py +0 -0
  288. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/weather.py +0 -0
  289. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/weather_api.py +0 -0
  290. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/weather_state.py +0 -0
  291. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/object.py +0 -0
  292. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/py.typed +0 -0
  293. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/__init__.py +0 -0
  294. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/observation.py +0 -0
  295. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/__init__.py +0 -0
  296. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/_executor.py +0 -0
  297. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/__init__.py +0 -0
  298. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/airmassconstraint.py +0 -0
  299. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/constraint.py +0 -0
  300. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/moonilluminationconstraint.py +0 -0
  301. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/moonseparationconstraint.py +0 -0
  302. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/solarelevationconstraint.py +0 -0
  303. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/timeconstraint.py +0 -0
  304. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/dataprovider.py +0 -0
  305. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/__init__.py +0 -0
  306. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/aftertime.py +0 -0
  307. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/beforetime.py +0 -0
  308. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/constant.py +0 -0
  309. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/follow.py +0 -0
  310. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/interval.py +0 -0
  311. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/merit.py +0 -0
  312. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/pernight.py +0 -0
  313. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/random.py +0 -0
  314. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/timewindow.py +0 -0
  315. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/transit.py +0 -0
  316. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/observationarchiveevolution.py +0 -0
  317. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/__init__.py +0 -0
  318. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/dynamictarget.py +0 -0
  319. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/heliocentricpolartarget.py +0 -0
  320. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/helioprojectivetarget.py +0 -0
  321. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/picker/__init__.py +0 -0
  322. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/picker/csvpicker.py +0 -0
  323. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/picker/picker.py +0 -0
  324. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/siderealtarget.py +0 -0
  325. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/target.py +0 -0
  326. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/__init__.py +0 -0
  327. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/__init__.py +0 -0
  328. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/skyflats.py +0 -0
  329. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/__init__.py +0 -0
  330. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/cases.py +0 -0
  331. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/conditional.py +0 -0
  332. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/parallel.py +0 -0
  333. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/selector.py +0 -0
  334. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/sequential.py +0 -0
  335. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/exceptions.py +0 -0
  336. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/__init__.py +0 -0
  337. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/transitimaging.py +0 -0
  338. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/script.py +0 -0
  339. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/__init__.py +0 -0
  340. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/callmodule.py +0 -0
  341. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/debugtrigger.py +0 -0
  342. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/log.py +0 -0
  343. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/__init__.py +0 -0
  344. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/filesystem/__init__.py +0 -0
  345. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/filesystem/observationarchive.py +0 -0
  346. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/filesystem/taskarchive.py +0 -0
  347. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/__init__.py +0 -0
  348. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/_portal.py +0 -0
  349. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/_schedulereader.py +0 -0
  350. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/_schedulewriter.py +0 -0
  351. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/configdb.py +0 -0
  352. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/mockobservationarchive.py +0 -0
  353. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/observationarchive.py +0 -0
  354. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/__init__.py +0 -0
  355. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/autofocus.py +0 -0
  356. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/default.py +0 -0
  357. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/script.py +0 -0
  358. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/task.py +0 -0
  359. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/taskarchive.py +0 -0
  360. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/taskrunner.py +0 -0
  361. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/memory/__init__.py +0 -0
  362. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/memory/observationarchive.py +0 -0
  363. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/memory/taskarchive.py +0 -0
  364. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/observationarchive.py +0 -0
  365. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/portal/__init__.py +0 -0
  366. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/taskrunner.py +0 -0
  367. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/__init__.py +0 -0
  368. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/__init__.py +0 -0
  369. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/archive.py +0 -0
  370. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/local_archive.py +0 -0
  371. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/pyobs_archive.py +0 -0
  372. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/calibration.py +0 -0
  373. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/exptime/__init__.py +0 -0
  374. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/exptime/exptime.py +0 -0
  375. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/exptime/stellarexptime.py +0 -0
  376. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/__init__.py +0 -0
  377. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/exptimeeval.py +0 -0
  378. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/flatfielder.py +0 -0
  379. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/pointing/__init__.py +0 -0
  380. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/pointing/base.py +0 -0
  381. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/pointing/static.py +0 -0
  382. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/__init__.py +0 -0
  383. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/archive.py +0 -0
  384. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/base.py +0 -0
  385. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/const.py +0 -0
  386. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/scheduler.py +0 -0
  387. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/__init__.py +0 -0
  388. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/average.py +0 -0
  389. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/cache.py +0 -0
  390. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/config.py +0 -0
  391. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/config_schema.py +0 -0
  392. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/coordinates.py +0 -0
  393. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/curvefit.py +0 -0
  394. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/enums.py +0 -0
  395. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/exceptions.py +0 -0
  396. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/exptime_grouping.py +0 -0
  397. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/fits.py +0 -0
  398. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/__init__.py +0 -0
  399. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/base.py +0 -0
  400. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/photometry.py +0 -0
  401. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/projection.py +0 -0
  402. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/__init__.py +0 -0
  403. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/filters.py +0 -0
  404. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/grid.py +0 -0
  405. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/gridnode.py +0 -0
  406. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/pipeline.py +0 -0
  407. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/__init__.py +0 -0
  408. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/__init__.py +0 -0
  409. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/binningwidget.py +0 -0
  410. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/datadisplaywidget.py +0 -0
  411. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/exposewidget.py +0 -0
  412. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/exposuretimewidget.py +0 -0
  413. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/imageformatwidget.py +0 -0
  414. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/listpickerdialog.py +0 -0
  415. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/windowingwidget.py +0 -0
  416. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/http.py +0 -0
  417. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/influxdb.py +0 -0
  418. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logger.py +0 -0
  419. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logging/__init__.py +0 -0
  420. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logging/context.py +0 -0
  421. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logging/resolvableerror.py +0 -0
  422. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/modulegui.py +0 -0
  423. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/__init__.py +0 -0
  424. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/applyaltazoffsets.py +0 -0
  425. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/applyoffsets.py +0 -0
  426. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/applyradecoffsets.py +0 -0
  427. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/parallel.py +0 -0
  428. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/__init__.py +0 -0
  429. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/pipeline.py +0 -0
  430. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/progress.py +0 -0
  431. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/reduction.py +0 -0
  432. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/reduction_base.py +0 -0
  433. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/__init__.py +0 -0
  434. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/csv.py +0 -0
  435. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/http.py +0 -0
  436. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/log.py +0 -0
  437. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/multi.py +0 -0
  438. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/publisher.py +0 -0
  439. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/serialization.py +0 -0
  440. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/shellcommand.py +0 -0
  441. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/threads/__init__.py +0 -0
  442. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/threads/lockwithabort.py +0 -0
  443. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/units.py +0 -0
  444. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/versions.py +0 -0
  445. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/version.py +0 -0
  446. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/__init__.py +0 -0
  447. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/archivefile.py +0 -0
  448. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/bufferedfile.py +0 -0
  449. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/filelists/__init__.py +0 -0
  450. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/filelists/filelist.py +0 -0
  451. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/filelists/testing.py +0 -0
  452. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/httpfile.py +0 -0
  453. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/memfile.py +0 -0
  454. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/sftpfile.py +0 -0
  455. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/smbfile.py +0 -0
  456. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/sshfile.py +0 -0
  457. {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/tempfile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-core
3
- Version: 2.3.0
3
+ Version: 2.4.1
4
4
  Summary: robotic telescope software
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -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:
@@ -203,13 +209,20 @@ class ImageWatcher(Module):
203
209
  async with self.vfs.open_file(filename, "rb") as fd:
204
210
  data = await fd.read()
205
211
 
206
- # try to load as fits file; data may well not be a FITS file at all
207
- try:
208
- with warnings.catch_warnings():
209
- warnings.simplefilter("ignore", fits.verify.VerifyWarning)
210
- fits_file = await asyncio.to_thread(fits.HDUList.fromstring, data)
211
- except Exception:
212
- 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
213
226
 
214
227
  # fill current file
215
228
  self.current_file = CurrentFile(filename=filename, data=data, hdu_list=fits_file)
@@ -259,6 +272,9 @@ class ImageWatcher(Module):
259
272
  log.info("Removing file from watch directory...")
260
273
  if not await self.vfs.remove(filename):
261
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)
262
278
 
263
279
  # cleanup extra
264
280
  await self.cleanup_extra(filename)
@@ -266,6 +282,23 @@ class ImageWatcher(Module):
266
282
  except Exception:
267
283
  log.exception("Something went wrong.")
268
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
+
269
302
  async def process_extra(self, filename: str) -> bool:
270
303
  """Can be overwritten by derived classes to do extra processing on files.
271
304
  All information are stored in self.current_file and can be checked against the given filename.
@@ -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"):
@@ -326,7 +334,10 @@ class Scheduler(Module, IRunnable, IRoboticScheduler):
326
334
  # schedule
327
335
  scheduled_tasks = ObservationList()
328
336
  first = True
329
- 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
+ ):
330
341
  # remember for later
331
342
  scheduled_tasks.append(scheduled_task)
332
343
 
@@ -0,0 +1,195 @@
1
+ """Planning-time instrument capability data sourced from pyobs-portal's `instruments` app.
2
+
3
+ Plain data models mirroring the shape `InstrumentSerializer` emits
4
+ (`pyobs_portal/instruments/serializers.py`) -- no Django import, this module only ever
5
+ deserializes the JSON the portal API already returns. Hand-entered planning data only, never a
6
+ live query against `ICamera`/`IBinning`/etc.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pydantic import Field
14
+
15
+ from pyobs.utils.serialization import BaseModel
16
+
17
+
18
+ class Filter(BaseModel):
19
+ """A single filter position in a filter wheel."""
20
+
21
+ name: str
22
+ position: int | None = None
23
+ updated_at: str | None = None
24
+
25
+
26
+ class BinningOption(BaseModel):
27
+ """Readout time for one binning mode of a camera."""
28
+
29
+ x: int
30
+ y: int
31
+ readout_time_s: float | None = None
32
+ updated_at: str | None = None
33
+
34
+
35
+ class FilterWheelCapability(BaseModel):
36
+ """Planning-time capability data for one filter wheel.
37
+
38
+ `module_name` is nullable -- not every filter wheel is its own addressable module (some
39
+ cameras expose filter selection through the camera module itself), per pyobs-portal#142.
40
+ """
41
+
42
+ name: str = ""
43
+ module_name: str | None = None
44
+ filter_change_time_s: float | None = None
45
+ updated_at: str | None = None
46
+ filters: list[Filter] = Field(default_factory=list)
47
+
48
+
49
+ class CameraCapability(BaseModel):
50
+ """Planning-time capability data for one camera, keyed by `module_name`."""
51
+
52
+ module_name: str
53
+ code: str
54
+ pixel_size_um: float | None = None
55
+ sensor_width_px: int | None = None
56
+ sensor_height_px: int | None = None
57
+ roi_min_width_px: int | None = None
58
+ roi_min_height_px: int | None = None
59
+ roi_step_px: int | None = None
60
+ exposure_time_min_s: float | None = None
61
+ exposure_time_max_s: float | None = None
62
+ image_types: list[str] = Field(default_factory=list)
63
+ updated_at: str | None = None
64
+ binnings: list[BinningOption] = Field(default_factory=list)
65
+ filter_wheels: list[FilterWheelCapability] = Field(default_factory=list)
66
+
67
+
68
+ # First-pass placeholder distance for slew-duration estimates -- there's no "current pointing"
69
+ # concept at estimate time, only a real slew *rate*, so this stands in for the actual
70
+ # destination-minus-start-position distance a real estimate would need. See
71
+ # specs/plans/2026-09-01-instrument-capability-duration-estimates.md's "Representative slew/rotate
72
+ # distance" note; pyobs-core#858/#859 track replacing this with a real distance.
73
+ #
74
+ # A default, not a hardcoded constant inside estimate_slew_time_s() below: #858/#859's real
75
+ # distances will likely differ per script (a flat-field PointingScript slew and an
76
+ # ImagingScript/AutoFocusScript target slew aren't the same kind of move), and this keeps that
77
+ # future per-caller distance a parameter on the estimate rather than a policy decision baked into
78
+ # what's meant to stay a pure mirror of the portal's data.
79
+ DEFAULT_SLEW_DISTANCE_DEG = 90.0
80
+
81
+
82
+ class TelescopeCapability(BaseModel):
83
+ """Planning-time capability data for one telescope, keyed by `module_name`."""
84
+
85
+ module_name: str
86
+ aperture_mm: float | None = None
87
+ focal_length_mm: float | None = None
88
+ mount_type: str = ""
89
+ slew_rate_deg_per_s: float | None = None
90
+ updated_at: str | None = None
91
+
92
+ def estimate_slew_time_s(self, distance_deg: float = DEFAULT_SLEW_DISTANCE_DEG) -> float | None:
93
+ """First-pass slew-duration estimate: `distance_deg` divided by the real slew rate. None
94
+ if `slew_rate_deg_per_s` isn't declared (or isn't positive) -- callers fall back to their
95
+ own flat constant in that case, same as when no capability data exists at all.
96
+
97
+ `distance_deg` defaults to `DEFAULT_SLEW_DISTANCE_DEG`, the same first-pass placeholder
98
+ every caller uses today; a caller can pass its own once #858/#859 give it a real,
99
+ script-specific distance instead of this shared guess.
100
+ """
101
+ if self.slew_rate_deg_per_s is None or self.slew_rate_deg_per_s <= 0:
102
+ return None
103
+ return distance_deg / self.slew_rate_deg_per_s
104
+
105
+
106
+ class DomeCapability(BaseModel):
107
+ """Planning-time capability data for one dome, keyed by `module_name`."""
108
+
109
+ module_name: str
110
+ rotate_rate_deg_per_s: float | None = None
111
+ updated_at: str | None = None
112
+
113
+
114
+ class Instrument(BaseModel):
115
+ """One telescope + dome + camera(s) grouping, as returned by `GET /api/instruments/`.
116
+
117
+ Purely an organizational grouping on the portal side -- it carries no module identity of its
118
+ own; each device below carries its own `module_name`. `InstrumentCapabilities` is what
119
+ flattens these into the module-name-keyed lookups scripts actually use.
120
+ """
121
+
122
+ display_name: str = ""
123
+ notes: str = ""
124
+ updated_at: str | None = None
125
+ cameras: list[CameraCapability] = Field(default_factory=list)
126
+ telescope: TelescopeCapability | None = None
127
+ dome: DomeCapability | None = None
128
+
129
+
130
+ class InstrumentCapabilities:
131
+ """Module-name-keyed view over a `GET /api/instruments/` response.
132
+
133
+ Scripts only ever need one device's capability row (by the module name they already
134
+ reference, e.g. `ImagingScript.camera`), never "the instrument" as a concept -- this flattens
135
+ the nested portal response into direct lookups once at parse time instead of every script
136
+ walking `instruments` and searching nested lists.
137
+ """
138
+
139
+ def __init__(self, instruments: list[Instrument]):
140
+ self.instruments = instruments
141
+ self._cameras: dict[str, CameraCapability] = {}
142
+ self._cameras_by_code: dict[str, CameraCapability] = {}
143
+ self._telescopes: dict[str, TelescopeCapability] = {}
144
+ self._domes: dict[str, DomeCapability] = {}
145
+ self._filter_wheels: dict[str, FilterWheelCapability] = {}
146
+
147
+ for instrument in instruments:
148
+ for camera in instrument.cameras:
149
+ self._cameras[camera.module_name] = camera
150
+ self._cameras_by_code[camera.code] = camera
151
+ for wheel in camera.filter_wheels:
152
+ if wheel.module_name is not None:
153
+ self._filter_wheels[wheel.module_name] = wheel
154
+ if instrument.telescope is not None:
155
+ self._telescopes[instrument.telescope.module_name] = instrument.telescope
156
+ if instrument.dome is not None:
157
+ self._domes[instrument.dome.module_name] = instrument.dome
158
+
159
+ @classmethod
160
+ def from_api_response(cls, data: list[dict[str, Any]]) -> InstrumentCapabilities:
161
+ """Parse `GET /api/instruments/`'s `results` list (already unwrapped by the caller)."""
162
+ return cls([Instrument.model_validate(item) for item in data])
163
+
164
+ def camera(self, module_name: str) -> CameraCapability | None:
165
+ """The `CameraCapability` whose own `module_name` matches, or None."""
166
+ return self._cameras.get(module_name)
167
+
168
+ def by_camera_code(self, code: str) -> CameraCapability | None:
169
+ """The `CameraCapability` with this fleet-wide physical camera code, or None."""
170
+ return self._cameras_by_code.get(code)
171
+
172
+ def telescope(self, module_name: str) -> TelescopeCapability | None:
173
+ """The `TelescopeCapability` whose own `module_name` matches, or None."""
174
+ return self._telescopes.get(module_name)
175
+
176
+ def dome(self, module_name: str) -> DomeCapability | None:
177
+ """The `DomeCapability` whose own `module_name` matches, or None."""
178
+ return self._domes.get(module_name)
179
+
180
+ def filter_wheel(self, module_name: str) -> FilterWheelCapability | None:
181
+ """The `FilterWheelCapability` whose own `module_name` matches, or None."""
182
+ return self._filter_wheels.get(module_name)
183
+
184
+
185
+ __all__ = [
186
+ "DEFAULT_SLEW_DISTANCE_DEG",
187
+ "Filter",
188
+ "BinningOption",
189
+ "FilterWheelCapability",
190
+ "CameraCapability",
191
+ "TelescopeCapability",
192
+ "DomeCapability",
193
+ "Instrument",
194
+ "InstrumentCapabilities",
195
+ ]
@@ -11,6 +11,7 @@ import astropy.units as u
11
11
  from astroplan import FixedTarget, ObservingBlock
12
12
 
13
13
  from pyobs.object import Object
14
+ from pyobs.robotic.instruments import InstrumentCapabilities
14
15
  from pyobs.utils.time import Time
15
16
 
16
17
  from .targets import SiderealTarget
@@ -44,8 +45,17 @@ class AstroplanScheduler(TaskScheduler):
44
45
  self._is_running: bool = False
45
46
 
46
47
  async def schedule(
47
- self, tasks: list[Task], projects: list[Project], start: Time, end: Time
48
+ self,
49
+ tasks: list[Task],
50
+ projects: list[Project],
51
+ start: Time,
52
+ end: Time,
53
+ instrument_capabilities: InstrumentCapabilities | None = None,
48
54
  ) -> AsyncIterator[Observation]:
55
+ # instrument_capabilities: accepted for TaskScheduler interface consistency, unused --
56
+ # this scheduler reads the stored Task.duration field rather than calling
57
+ # estimate_duration() live (see specs/plans/2026-09-01-instrument-capability-duration-estimates.md
58
+ # Non-goals).
49
59
  # is lock acquired? send abort signal
50
60
  if self._lock.locked():
51
61
  await self.abort()
@@ -10,6 +10,7 @@ import numpy as np
10
10
  from astropy.time import TimeDelta
11
11
 
12
12
  from pyobs.object import Object
13
+ from pyobs.robotic.instruments import InstrumentCapabilities
13
14
  from pyobs.robotic.storage.observationarchive import ObservationArchive
14
15
  from pyobs.utils.time import Time
15
16
 
@@ -59,7 +60,12 @@ class OnDemandScheduler(TaskScheduler):
59
60
  self._global_constraints: list[Constraint] = [Constraint.create(self, c) for c in constraints]
60
61
 
61
62
  async def schedule(
62
- self, tasks: list[Task], projects: list[Project], start: Time, end: Time
63
+ self,
64
+ tasks: list[Task],
65
+ projects: list[Project],
66
+ start: Time,
67
+ end: Time,
68
+ instrument_capabilities: InstrumentCapabilities | None = None,
63
69
  ) -> AsyncIterator[Observation]:
64
70
  if self._observer is None:
65
71
  raise RuntimeError("No observer given.")
@@ -75,7 +81,9 @@ class OnDemandScheduler(TaskScheduler):
75
81
  data.archive.freeze()
76
82
 
77
83
  # schedule from start to end
78
- async for task in self.schedule_in_interval(tasks, projects_dict, start, end, data):
84
+ async for task in self.schedule_in_interval(
85
+ tasks, projects_dict, start, end, data, instrument_capabilities=instrument_capabilities
86
+ ):
79
87
  # evolve archive -- night is keyed by the task's own scheduled time, not "now", since
80
88
  # a run can schedule up to schedule_range ahead into a different night (see
81
89
  # ObservationArchiveEvolution.evolve()'s docstring/comment for why)
@@ -95,6 +103,7 @@ class OnDemandScheduler(TaskScheduler):
95
103
  end: Time,
96
104
  data: DataProvider,
97
105
  step: float = 300,
106
+ instrument_capabilities: InstrumentCapabilities | None = None,
98
107
  ) -> AsyncIterator[Observation]:
99
108
  time = start
100
109
  while time < end:
@@ -105,7 +114,9 @@ class OnDemandScheduler(TaskScheduler):
105
114
  task.reset_resolved_target()
106
115
 
107
116
  # schedule first in this interval, could be one or two
108
- async for scheduled_task in self.schedule_first_in_interval(tasks, projects, time, end, data): # type: ignore[arg-type]
117
+ async for scheduled_task in self.schedule_first_in_interval(
118
+ tasks, projects, time, end, data, instrument_capabilities=instrument_capabilities # type: ignore[arg-type]
119
+ ):
109
120
  # yield it to caller
110
121
  yield scheduled_task
111
122
 
@@ -128,44 +139,73 @@ class OnDemandScheduler(TaskScheduler):
128
139
  end: Time,
129
140
  data: DataProvider,
130
141
  step: float = 300,
142
+ instrument_capabilities: InstrumentCapabilities | None = None,
131
143
  ) -> AsyncIterator[Observation]:
132
144
  # find current best task
133
- task, merit = await self.find_next_best_task(tasks, projects, start, end, data)
145
+ task, merit = await self.find_next_best_task(
146
+ tasks, projects, start, end, data, instrument_capabilities=instrument_capabilities
147
+ )
134
148
 
135
149
  if task is not None and merit is not None:
136
150
  # check, whether there is another task within its duration that will have a higher merit
137
151
  better_task, better_time, better_merit = await self.check_for_better_task(
138
- task, projects, merit, tasks, start, end, data, step=step
152
+ task,
153
+ projects,
154
+ merit,
155
+ tasks,
156
+ start,
157
+ end,
158
+ data,
159
+ step=step,
160
+ instrument_capabilities=instrument_capabilities,
139
161
  )
140
162
 
141
163
  if better_task is not None and better_time is not None and better_merit is not None:
142
164
  # can we maybe postpone the better task to run both?
143
165
  postpone_time = await self.can_postpone_task(
144
- task, projects, better_task, better_merit, start, end, data
166
+ task,
167
+ projects,
168
+ better_task,
169
+ better_merit,
170
+ start,
171
+ end,
172
+ data,
173
+ instrument_capabilities=instrument_capabilities,
145
174
  )
146
175
  if postpone_time is not None:
147
176
  # yes, we can! schedule both
148
- yield self.create_scheduled_task(task, merit, start)
149
- yield self.create_scheduled_task(better_task, better_merit, postpone_time)
177
+ yield self.create_scheduled_task(
178
+ task, merit, start, instrument_capabilities=instrument_capabilities
179
+ )
180
+ yield self.create_scheduled_task(
181
+ better_task, better_merit, postpone_time, instrument_capabilities=instrument_capabilities
182
+ )
150
183
  else:
151
184
  # just schedule better_task
152
- yield self.create_scheduled_task(better_task, better_merit, better_time)
185
+ yield self.create_scheduled_task(
186
+ better_task, better_merit, better_time, instrument_capabilities=instrument_capabilities
187
+ )
153
188
 
154
189
  # and find other tasks for in between, new end time is better_time
155
- async for between_task in self.schedule_in_interval(tasks, projects, start, better_time, data):
190
+ async for between_task in self.schedule_in_interval(
191
+ tasks, projects, start, better_time, data, instrument_capabilities=instrument_capabilities
192
+ ):
156
193
  yield between_task
157
194
 
158
195
  else:
159
196
  # this seems to be the best task for now, schedule it
160
- yield self.create_scheduled_task(task, merit, start)
197
+ yield self.create_scheduled_task(task, merit, start, instrument_capabilities=instrument_capabilities)
161
198
 
162
- def create_scheduled_task(self, task: Task, merit: float, time: Time) -> Observation:
199
+ def create_scheduled_task(
200
+ self, task: Task, merit: float, time: Time, instrument_capabilities: InstrumentCapabilities | None = None
201
+ ) -> Observation:
163
202
  from pyobs.robotic import Observation
164
203
 
165
204
  return Observation(
166
205
  task=task,
167
206
  start=time,
168
- end=time + TimeDelta(task.estimate_duration(time=time) * u.second),
207
+ end=time
208
+ + TimeDelta(task.estimate_duration(time=time, instrument_capabilities=instrument_capabilities) * u.second),
169
209
  priority=merit,
170
210
  target=task.target,
171
211
  )
@@ -213,7 +253,13 @@ class OnDemandScheduler(TaskScheduler):
213
253
  return total_merit
214
254
 
215
255
  async def evaluate_constraints_and_merits(
216
- self, tasks: list[Task], projects: dict[str, Project], start: Time, end: Time, data: DataProvider
256
+ self,
257
+ tasks: list[Task],
258
+ projects: dict[str, Project],
259
+ start: Time,
260
+ end: Time,
261
+ data: DataProvider,
262
+ instrument_capabilities: InstrumentCapabilities | None = None,
217
263
  ) -> list[float]:
218
264
  # evaluate all merit functions at given time
219
265
  merits: list[float] = []
@@ -230,7 +276,13 @@ class OnDemandScheduler(TaskScheduler):
230
276
  # no merits? evaluate to 1
231
277
  merit = 1.0
232
278
 
233
- elif start + TimeDelta(task.estimate_duration(time=start) * u.second) > end:
279
+ elif (
280
+ start
281
+ + TimeDelta(
282
+ task.estimate_duration(time=start, instrument_capabilities=instrument_capabilities) * u.second
283
+ )
284
+ > end
285
+ ):
234
286
  # if task is too long for the given slot, we evaluate its merits to zero
235
287
  merit = 0.0
236
288
 
@@ -255,11 +307,19 @@ class OnDemandScheduler(TaskScheduler):
255
307
  return merits
256
308
 
257
309
  async def find_next_best_task(
258
- self, tasks: list[Task], projects: dict[str, Project], start: Time, end: Time, data: DataProvider
310
+ self,
311
+ tasks: list[Task],
312
+ projects: dict[str, Project],
313
+ start: Time,
314
+ end: Time,
315
+ data: DataProvider,
316
+ instrument_capabilities: InstrumentCapabilities | None = None,
259
317
  ) -> tuple[Task | None, float]:
260
318
 
261
319
  # evaluate all merit functions at given time
262
- merits = await run_cpu_bound(self.evaluate_constraints_and_merits, tasks, projects, start, end, data)
320
+ merits = await run_cpu_bound(
321
+ self.evaluate_constraints_and_merits, tasks, projects, start, end, data, instrument_capabilities
322
+ )
263
323
 
264
324
  # find max one
265
325
  idx = np.argmax(merits)
@@ -278,12 +338,17 @@ class OnDemandScheduler(TaskScheduler):
278
338
  end: Time,
279
339
  data: DataProvider,
280
340
  step: float = 300,
341
+ instrument_capabilities: InstrumentCapabilities | None = None,
281
342
  ) -> tuple[Task | None, Time | None, float | None]:
282
343
  # exclude the already-selected task so it can't be picked as "better"
283
344
  other_tasks = [t for t in tasks if t is not task]
284
345
  t = start + TimeDelta(step * u.second)
285
- while t < start + TimeDelta(task.estimate_duration(time=start) * u.second):
286
- merits = await run_cpu_bound(self.evaluate_constraints_and_merits, other_tasks, projects, t, end, data)
346
+ while t < start + TimeDelta(
347
+ task.estimate_duration(time=start, instrument_capabilities=instrument_capabilities) * u.second
348
+ ):
349
+ merits = await run_cpu_bound(
350
+ self.evaluate_constraints_and_merits, other_tasks, projects, t, end, data, instrument_capabilities
351
+ )
287
352
  for i, m in enumerate(merits):
288
353
  if m > merit:
289
354
  return other_tasks[i], t, m
@@ -299,13 +364,24 @@ class OnDemandScheduler(TaskScheduler):
299
364
  start: Time,
300
365
  end: Time,
301
366
  data: DataProvider,
367
+ instrument_capabilities: InstrumentCapabilities | None = None,
302
368
  ) -> Time | None:
303
369
  # new start time of better_task would be after the execution of task
304
- better_start: Time = start + TimeDelta(task.estimate_duration(time=start) * u.second)
370
+ better_start: Time = start + TimeDelta(
371
+ task.estimate_duration(time=start, instrument_capabilities=instrument_capabilities) * u.second
372
+ )
305
373
 
306
374
  # evaluate merit of better_task at new start time
307
375
  merit = (
308
- await run_cpu_bound(self.evaluate_constraints_and_merits, [better_task], projects, better_start, end, data)
376
+ await run_cpu_bound(
377
+ self.evaluate_constraints_and_merits,
378
+ [better_task],
379
+ projects,
380
+ better_start,
381
+ end,
382
+ data,
383
+ instrument_capabilities,
384
+ )
309
385
  )[0]
310
386
 
311
387
  # if it got better, return it, otherwise return Nones
@@ -6,6 +6,7 @@ from collections.abc import AsyncIterator
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from pyobs.object import Object
9
+ from pyobs.robotic.instruments import InstrumentCapabilities
9
10
  from pyobs.utils.time import Time
10
11
 
11
12
  if TYPE_CHECKING:
@@ -19,7 +20,12 @@ class TaskScheduler(Object, metaclass=abc.ABCMeta):
19
20
 
20
21
  @abc.abstractmethod
21
22
  async def schedule(
22
- self, tasks: list[Task], projects: list[Project], start: Time, end: Time
23
+ self,
24
+ tasks: list[Task],
25
+ projects: list[Project],
26
+ start: Time,
27
+ end: Time,
28
+ instrument_capabilities: InstrumentCapabilities | None = None,
23
29
  ) -> AsyncIterator[Observation]:
24
30
  # if we don't yield once here, mypy doesn't like this, see:
25
31
  # https://github.com/python/mypy/issues/5385
@@ -282,9 +282,20 @@ class DarkBiasScript(Script):
282
282
  peek_cached_science_exptimes_for_night(). Falls back to a placeholder single-series
283
283
  estimate at _FALLBACK_MATCH_EXPTIME if nothing is cached yet -- e.g. can_run() hasn't
284
284
  run for this site/night within the cache's TTL.
285
+
286
+ readout uses the camera's matching BinningOption.readout_time_s (data.instrument_capabilities)
287
+ when available, falling back to the flat 5.0 s fudge otherwise.
285
288
  """
286
- # TODO: get a better estimate for readout overhead
287
289
  readout = 5.0
290
+ capabilities = data.instrument_capabilities if data is not None else None
291
+ camera = capabilities.camera(self.camera) if capabilities is not None else None
292
+ if camera is not None:
293
+ x, y = self.binning
294
+ for binning_option in camera.binnings:
295
+ if binning_option.x == x and binning_option.y == y and binning_option.readout_time_s is not None:
296
+ readout = binning_option.readout_time_s
297
+ break
298
+
288
299
  if self.exptimes is not None:
289
300
  return sum(self.count * (e + readout) for e in self.exptimes)
290
301
 
@@ -51,8 +51,10 @@ class PointingScript(Script):
51
51
 
52
52
  def estimate_duration(self, data: TaskData | None = None, time: Time | None = None) -> float:
53
53
  """Estimate duration of slewing to the flat-field pointing."""
54
- # TODO: get a better estimate for slewing
55
- return 60.0
54
+ capabilities = data.instrument_capabilities if data is not None else None
55
+ telescope = capabilities.telescope(self.telescope) if capabilities is not None else None
56
+ slew_time = telescope.estimate_slew_time_s() if telescope is not None else None
57
+ return slew_time if slew_time is not None else 60.0
56
58
 
57
59
 
58
60
  __all__ = ["PointingScript"]
@@ -90,8 +90,10 @@ class AutoFocusScript(Script):
90
90
 
91
91
  def estimate_duration(self, data: TaskData | None = None, time: Time | None = None) -> float:
92
92
  """Estimate duration of the autofocus run."""
93
- # TODO: get a better estimate for slewing
94
- return self.count * self.exposure_time + 60.0
93
+ capabilities = data.instrument_capabilities if data is not None else None
94
+ telescope = capabilities.telescope(self.telescope) if capabilities is not None else None
95
+ slew_time = telescope.estimate_slew_time_s() if telescope is not None else None
96
+ return self.count * self.exposure_time + (slew_time if slew_time is not None else 60.0)
95
97
 
96
98
 
97
99
  __all__ = ["AutoFocusScript"]