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.
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/PKG-INFO +1 -1
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/imagewatcher.py +40 -7
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/scheduler.py +18 -7
- pyobs_core-2.4.1/pyobs/robotic/instruments.py +195 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/astroplanscheduler.py +11 -1
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/ondemandscheduler.py +97 -21
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/taskscheduler.py +7 -1
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/darkbias.py +12 -1
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/pointing.py +4 -2
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/autofocus.py +4 -2
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/imaging.py +53 -4
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/portal/observationarchive.py +12 -9
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/portal/taskarchive.py +54 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/taskarchive.py +8 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/task.py +8 -2
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/time.py +10 -3
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/file.py +12 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/localfile.py +31 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/vfs.py +20 -1
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyproject.toml +1 -1
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/.gitignore +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/LICENSE +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/application.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/background_task.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/_cli.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/pyobs.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/pyobsd.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/cli/pyobsw.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/comm.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/commlogging.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/dummy/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/dummy/dummycomm.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/local/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/local/localcomm.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/local/localnetwork.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/proxy.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/rpc.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/serializer.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009/binding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009/rpc.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/rpc.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/stanza/RPC.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xep_0009_timeout/stanza/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xmppclient.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/comm/xmpp/xmppcomm.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/badweather.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/event.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/exposurestatuschanged.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/filterchanged.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/focusfound.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/goodweather.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/log.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/modechanged.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/moduleclosed.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/moduleopened.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/motionstatuschanged.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/move.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/newimage.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/newspectrum.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/offsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/roofclosing.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/roofopened.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/taskfailed.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/taskfinished.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/taskstarted.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/events/testevent.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/image.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/altazoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/exptime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/genericoffset.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/onskydistance.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/pixeloffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/radecoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/meta/skyoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processor.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/_daobackgroundremover.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/_pillow.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/circle.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/crosshair.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/annotation/text.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_request.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_request_builder.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_request_logger.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/_dotnet_response_saver.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/astrometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/astrometry/dotnet.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/_calibration_cache.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/_ccddata_calibrator.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/calibration/calibration.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/_pysep_stats_calculator.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/_source_catalog.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/daophot.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/pysep.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/simpledisk.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/detection/sourcedetection.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/exptime/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/exptime/exptime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/exptime/star.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/addfitsheaders.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/download.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/flip.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/grayscale.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/httpserver.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/normalize.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/save.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/saveimage.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/smooth.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/image/softbin.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/addmask.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/catalogcircularmask.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/circularmask.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/createfilename.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/imagesourcefilter.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/misc/removebackground.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/modules/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/modules/getfitsheaders.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/astrometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/brighteststar.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/brighteststar_guiding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/dummyoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/dummyskyoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/fitsheader.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/offsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/pixeloffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/projected.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/offsets/spilled_light.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/_photometry_calculator.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/_photutil_aperture_photometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/_sep_aperture_photometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/aperture_photometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/photometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/photutil.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/photometry/pysep.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/wcs/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/images/processors/wcs/solarhelioprojective.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAbortable.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAcquisition.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAutoFocus.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAutoGuiding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IAutonomous.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IBinning.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ICalibrate.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ICamera.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IConfig.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ICooling.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IData.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IDataSequence.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IDome.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IExposure.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IExposureTime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFilters.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFitsHeaderAfter.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFitsHeaderBefore.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFlatField.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFocusModel.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IFocuser.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IGain.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IImageFormat.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IImageType.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IMode.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IModule.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IMotion.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IMultiFiber.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IOffsetsAltAz.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IOffsetsRaDec.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingAltAz.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingBody.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingHeliocentricPolar.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingHeliographicStonyhurst.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingHelioprojective.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingOrbitalElements.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingRaDec.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IPointingSeries.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IReady.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRobotic.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRoboticScheduler.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRoof.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRotation.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRunnable.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IRunning.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IScriptRunner.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ISpectrograph.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IStartStop.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IStructuredConfig.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ISyncTarget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITelescope.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITemperatures.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITrackingMode.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/ITrackingRate.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IVideo.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IWeather.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/IWindow.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/interfaces/interface.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/camerasettings.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/fitsheader.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/fitsnamespace.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/follow.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/motionstatus.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/pipeline.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/waitformotion.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/mixins/weatheraware.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/adaptive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/basecamera.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/basespectrograph.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/basevideo.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/dummycamera.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/dummyspectrograph.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/dummyvideo.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/camera/pipelinecamera.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/flatfield.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/pointing.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/flatfield/scheduler.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/dummyautofocus.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/focusmodel.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/focus/focusseries.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/imagewriter.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/pipeline.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/image/seeing.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/module.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/_base.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/_baseguiding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/acquisition.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/autoguiding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/dummyacquisition.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/dummyguiding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/guidingstatistics.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/pixeloffset.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/skyoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/guidingstatistics/uptime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/pointing/scienceframeguiding.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/dummymastermind.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/dummyscheduler.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/mastermind.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/pointing.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/robotic/scriptrunner.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/basedome.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/baseroof.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/roof/dummyroof.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/_dummytelescopebase.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/basetelescope.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/dummyaltaztelescope.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/dummyradectelescope.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/telescope/dummysolartelescope.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/test/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/test/standalone.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/dummymode.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/dummystructuredconfig.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/fluentlogger.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/httpfilecache.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/kiosk.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/matrix.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/stellarium.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/telegram.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/utils/trigger.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/mockweather.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/weather.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/weather_api.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/modules/weather/weather_state.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/object.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/py.typed +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/observation.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/_executor.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/airmassconstraint.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/constraint.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/moonilluminationconstraint.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/moonseparationconstraint.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/solarelevationconstraint.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/constraints/timeconstraint.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/dataprovider.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/aftertime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/beforetime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/constant.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/follow.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/interval.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/merit.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/pernight.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/random.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/timewindow.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/merits/transit.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/observationarchiveevolution.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/dynamictarget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/heliocentricpolartarget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/helioprojectivetarget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/picker/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/picker/csvpicker.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/picker/picker.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/siderealtarget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scheduler/targets/target.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/calibration/skyflats.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/cases.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/conditional.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/parallel.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/selector.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/control/sequential.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/exceptions.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/imaging/transitimaging.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/script.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/callmodule.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/debugtrigger.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/scripts/utils/log.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/filesystem/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/filesystem/observationarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/filesystem/taskarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/_portal.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/_schedulereader.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/_schedulewriter.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/configdb.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/mockobservationarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/observationarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/autofocus.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/default.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/scripts/script.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/task.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/taskarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/lco/taskrunner.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/memory/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/memory/observationarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/memory/taskarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/observationarchive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/storage/portal/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/taskrunner.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/archive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/local_archive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/archive/pyobs_archive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/calibration.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/exptime/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/exptime/exptime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/exptime/stellarexptime.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/exptimeeval.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/flatfielder.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/pointing/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/pointing/base.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/pointing/static.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/archive.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/base.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/priorities/const.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/robotic/utils/skyflats/scheduler.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/average.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/cache.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/config.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/config_schema.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/coordinates.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/curvefit.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/enums.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/exceptions.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/exptime_grouping.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/fits.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/base.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/photometry.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/focusseries/projection.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/filters.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/grid.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/gridnode.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/grids/pipeline.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/binningwidget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/datadisplaywidget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/exposewidget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/exposuretimewidget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/imageformatwidget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/listpickerdialog.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/gui/camera/windowingwidget.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/http.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/influxdb.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logger.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logging/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logging/context.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/logging/resolvableerror.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/modulegui.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/applyaltazoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/applyoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/offsets/applyradecoffsets.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/parallel.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/pipeline.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/progress.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/reduction.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/pipeline/reduction_base.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/csv.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/http.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/log.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/multi.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/publisher/publisher.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/serialization.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/shellcommand.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/threads/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/threads/lockwithabort.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/units.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/utils/versions.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/version.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/archivefile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/bufferedfile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/filelists/__init__.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/filelists/filelist.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/filelists/testing.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/httpfile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/memfile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/sftpfile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/smbfile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/sshfile.py +0 -0
- {pyobs_core-2.3.0 → pyobs_core-2.4.1}/pyobs/vfs/tempfile.py +0 -0
|
@@ -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
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
except
|
|
212
|
-
|
|
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
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
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, "
|
|
115
|
-
extra_schedule_kwargs["
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
149
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
286
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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"]
|