Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Dates are specified in the format DD-MM-YYYY.
[Unreleased]¶
Added¶
Service,STARTUP_TIMEOUTandSTOP_TIMEOUT(redsun.services) - the handle a container makes for each service it declares. A service with a module runs aspython -m <module> <args>:startwaits up toSTARTUP_TIMEOUTseconds for its readiness line, logs its output atDEBUGonredsun.service.<name>, and gives it a Channel Access server port of its own, appended toEPICS_CA_ADDR_LIST.stopcloses the process's standard input, then sendsSIGINTon POSIX, then kills it, each step waitingstop_timeoutseconds,STOP_TIMEOUT(10 s) by default. A ready service exiting unasked logs its exit code and last 20 output lines atERRORand emitssig_exited(name, code). A service without a module is attached to and has nothing to start or stop.declare_service(redsun.containers,redsun) - declares a service on a container. One with amoduleis launched aspython -m <module> <args>; one without only lends itsprefix:
class MyApp(AppContainer):
camera_ioc = declare_service(
module="mylab.iocs.camera", ready="Server startup complete.", prefix="CAM:"
)
camera = declare_device(MyCamera, service="camera_ioc")
servicekeyword of a device declaration - names the service whose prefix the device is built with, passed asprefix. Givingprefixas well, or naming a service for a device whose constructor takes aservicekeyword of its own, is refused at declaration. A device naming a service that did not start, one that is not declared, or one that gives no prefix, is logged and skipped by the build.AppContainer.start_servicesandAppContainer.services(redsun.containers.container) - start every launched service, loggingServices started: <n>/<m>and the ones that did not start; and the container's services by name.buildcallsstart_servicesas well; only the first call beforeshutdownstarts anything.- A
servicessection in a session file, and aservicesgroup in a plugin manifest giving a service'smoduleandreadyline. A session entry withplugin_nameandplugin_idtakes both from the manifest; one without is attached to:
services:
camera_ioc:
plugin_name: mylab
plugin_id: camera-ioc
prefix: "CAM:"
stop_timeout: 60
beamline:
prefix: "BL01:"
- The build summary names a service whose every device failed to build:
Unused: camera_ioc (no device built). - An
epicsextra and dependency group, withcaprotoandophyd-async[ca]. autoconnectkeyword of a device declaration, true unless given - whether the build connects the device. The build connects every such device at once, in a"connect"step between devices and presenters, waiting up toCONNECT_TIMEOUT(redsun.containers.container, 10 s) for each. A device that does not connect is logged and skipped, listed as<name> (device, not connected)in the build summary, with the service it talks to named in the message.service_of(redsun.log) - the name of the service a record came from,Nonefor the application.BufferHandler.service_records,BufferHandler.servicesandBufferHandler.service_capacity(redsun.log) - one service's retained records or every service's merged by time, the services that have logged, and how many records of each are retained, 2 000 by default.- A launched service's log file,
<run>.<service>.logbeside the application's, opened by the container and created once the service logs something.SessionFileHandlertakes aserviceand arun, andSessionFileHandler.runnames the run a file belongs to;add_handler,remove_handlerandsession_logtake aservice. - A line of a service's output that is a JSON log record, written by a stdlib
formatter or by
loguruwithserialize=True, is logged with its own level, time and traceback underredsun.service.<service>.<logger>. LogView(redsun.view.qt.builtins) shows services' records on a Services tab, with a selector for one service or all of them.SessionPathProvider,PlanFilenameProviderandsession_directory(redsun.path_provider), moved fromredsun.storage.session_directory(session)returns<base_dir>/<session>.path_providerkeyword of a device declaration, reserved by the container: a device whose constructor takes it gets the session's provider, and a declaration giving one is refused:
AppContainer.path_provider(redsun.containers.container) - the session's provider, wired underPATH_PROVIDER_PORT("path_provider"), withset_plan,reset_planandset_base_diras slots:
StorageConfigandCatalogConfig(redsun.containers) and astoragesection in a session file:base_dir, the root a session writes under (user_data_dir("redsun", appauthor=False)by default),max_digits, the width of the file counter, andcatalog, which, even empty, gives the session a catalog in<base_dir>/<session>/catalogand needs thetiledextra:
storage:
base_dir: "D:/experiments/2026-09" # optional
catalog: # optional
readable: # optional, added to <base_dir>/<session>
- /data/aht
AppContainer.storage gives the section after the build. readable adds
directories the catalog may read from. Unknown keys are refused, and so is a
catalog without the tiled extra.
SessionPathProvider.session_dir(redsun.path_provider) - the session's directory insidebase_dir, holding its files and catalog.SessionPathProvider.lock_base_dir(redsun.path_provider) - makes every laterset_base_dirraiseRuntimeErrorwith the given reason. A session with a catalog locks its provider once the catalog starts.SessionPathProvider.base_dirandSessionPathProvider.set_base_dir, a slot taking astror aPath, effective from the next request. It raisesRuntimeErrorwhile a plan runs.PATH_PROVIDER(redsun.path_provider) - key the container binds the session's provider to:
redsun.storage.writers- one module per format, each withwrite(uri, *, data_key, data, metadata=None) -> str, adding a derived product to an acquisition's store:zarrforapplication/x-zarr,ome_zarrforapplication/x-ome-zarr:
from redsun.storage.writers import ome_zarr
product_uri = ome_zarr.write(
resource["uri"],
data_key="det_median",
data=median,
metadata={"derived_from": resource["data_key"]},
)
The returned URI is the argument when the product joined that store, and a
new one when it went beside it, as it does for a root carrying OME-Zarr
metadata (an image, a plate, a bioformats2raw layout), which zarr.write
refuses. A writer registers nothing.
- WriterError (redsun.storage.writers) - raised for a store the
writer cannot take, an array with too few or too many dimensions, or a
missing package, naming the extra that installs it.
- A zarr extra and dependency group, with ome-writers[acquire-zarr].
- A tiled extra and dependency group, with tiled[client,server] and
ome-tiled[bluesky]. It installs nothing on Python 3.14.
- CatalogAddress and CATALOG (redsun.catalog) - where a
session's catalog is served, and its key. CatalogAddress.uri carries the
API key, which the repr leaves out. The module imports nothing from
tiled:
from redsun.catalog import CATALOG
from tiled.client import from_uri
address = container.try_require(CATALOG) # None without a catalog
client = from_uri(address.uri) if address else None
- A session whose
storagesection has acatalogstarts atiledserver with its virtual container, provides its address underCATALOG, and stops it onshutdownafter the presenters. The server keeps its database in<base_dir>/<session>/catalogand reads assets from<base_dir>/<session>and everyreadabledirectory, checked on read. A catalog that fails to start is logged and skipped.application/x-ome-zarrassets are read withome-tiled'sOmeZarrAdapter, andome-tiled's consolidator is registered forTiledWriter, which then stores an OME-Zarr image with its store's shape and axis names.
Changed¶
- A session's data, catalog and log folders take the session name with each
run of characters other than letters, digits,
.,-and_replaced by_and outer dots removed:Lab A: STEDbecomesLab_A_STED. The data folder used the name unchanged; earlier files stay where they are. AppContainer.BUILD_STEPS(redsun.containers.container) starts with"services", so aduring_buildhook reports services starting, and has"connect"after"devices". A build that raises stops the services before the exception propagates.AppContainer.runno longer callsconnect_devices; the build connects the devices declared withautoconnect.connect_devicesconnects every device, whatever itsautoconnectsays.- A service keeps its Channel Access server port for as long as the process runs, and a container stopping the services it launched closes the process's Channel Access channels, so a container built again in the same process reaches its services at once.
AppContainer.shutdownstops the container's services, the last declared first, whether or not the container was built, and before it closes the session log file.BufferHandler(redsun.log) retains application records and each service's records apart, each dropping its oldest once full.BufferHandler.recordsholds the application's records only.SessionFileHandler(redsun.log) for the application no longer writes services' records, and pruning old runs counts a run's service files with it.GlobalFormatter(redsun.log) leaves out the location of a record that carries none, such as one rebuilt from a service's output.LogView(redsun.view.qt.builtins):Save logs...andClear log windowact on the tab shown.SessionPathProvidergives each data key its own directory,<base_dir>/<session>/<YYYY-MM-DD>/<datakey>/<plan>_<counter>, counting per(plan, datakey). A call without a data key leaves that level out.PlanFilenameProvider.bumptakes the data key as its second argument andPlanFilenameProvider.resettakes(plan, datakey)keys.SessionPathProviderwrites underuser_data_dir("redsun", appauthor=False)rather than~/redsun-storage. Nothing is moved; a provider built while~/redsun-storageexists logs aWARNINGnaming both locations.
Removed¶
DescriptorTreeView.get_keys(redsun.view.qt), which nothing called.HasAsyncShutdownand theredsun.devicepackage, which held nothing else. No container ever calledshutdownon a device.- The
redsun.storageshim:BaseStorage,StreamSpec,OpenStore,StorageIO,SinkFactory,StoreStateError,FrameSink,PathSignals, the storage registry (register_storage,get_storage,clear_registry,reset_group) and theacquire-zarrand in-memory backends; see ADR 0013.StreamDatumranges are whatever the device emits. StoragePresenter(redsun.presenter.builtins) andStorageView(redsun.view.qt.builtins), with their manifest entries. A session declaring them drops both and givesbase_dirin thestoragesection.redsun.storage.PATH_PROVIDERmoves toredsun.path_provider.- The
zarrextra and dependency group installome-writers[acquire-zarr]instead ofacquire-zarr.
Changed (breaking)¶
AppContainer.build(redsun.containers.container) constructs a device ascls(name=<name>, **kwargs)rather thancls(<name>, **kwargs). A device subclassingophyd_async.epics.core.EpicsDevice, whose first parameter isprefix, can be declared withdeclare_device(MyCamera, prefix="CAM:"). A device constructor takingnamepositional-only fails to build; drop the/:
0.12.3 - 13-09-2026¶
Added¶
BufferHandlerandlog_buffer()(redsun.log) - the session's log records, retained as they are emitted. The handler is installed on theredsunlogger alongside the stdout one and keeps the most recent 10 000 records, so a consumer built later in the session can still show what happened before it existed.BufferHandler.capacityis how many records it keeps.LogView(redsun.view.qt.builtins) - a read-only console showing those records, colour-coded by level in one of two sets chosen from the console's own background, so the text keeps its contrast under a light and a dark palette alike, and redrawn when the palette changes. Buttons choose the lowest level displayed, redrawing from the buffer so raising the threshold never discards anything, andSave logs...writes every record of the run regardless of what is on screen, copied from the session's log file when one is open and taken from the buffer otherwise.Open log folderopens the folder holding the session's log files, and is disabled when no log file is open. Records arriving while it is open are drawn in batches every 100 ms, at most 2 000 per batch, and the console keeps no more lines than the buffer holds records. Available from a configuration file asplugin_name: redsun,plugin_id: logsunderviews.SessionFileHandlerandsession_log()(redsun.log) - a file of each run's log records, at<user log directory>/redsun/<session>/<start time>_<pid>.log. The file is rotated at 10 MB with 5 older files kept, and opening one deletes the files of all but the 20 most recent runs of that session.session_log()returns the handler installed on theredsunlogger, orNone.AppContainer(redsun.containers) opens aSessionFileHandlerfor its session when it is constructed and when it is built after a shutdown, andshutdown()closes it.
Changed¶
RunEngine(redsun.engine) runs each plan, and eachresume, on a thread of its own namedRunEngine, which ends with the plan, instead of on a thread pool kept for the engine's lifetime. A plan submitted while another is running fails withbluesky's error instead of waiting its turn.
Fixed¶
RunEngine(redsun.engine) takesloop=Noneand falls back to the shared background loop when an engine is built, so importingredsun.engineno longer starts the loop and its thread.GlobalFormatter.format(redsun.log) appends the traceback of a record carrying one, and the stack of a record logged withstack_info=True, so alogger.exception(...)call reaches stdout andLogViewwith both.
Removed¶
redsun.common.qt.ask_file_pathand theredsun.commonpackage.QtAppContainer's main window no longer has aFilemenu or itsSave configuration as...action, which wrote no file.
0.12.2 - 07-09-2026¶
Fixed¶
QtAppContainer.shutdown(redsun.containers.qt) stops the timer drainingpsygnal's emission queue and delivers what is left in it before destroying the widgets. An emission queued for a slot with a thread affinity reached a destroyed widget asRuntimeError: wrapped C/C++ object of type <widget> has been deleted, and carried into the next container built in the same process.
0.12.1 - 03-09-2026¶
Added¶
ComponentNotBuilt(redsun.virtual) - theWiringErrorVirtualContainer.connect_pathsraises for a port path naming a component that is not there. It carries the name ascomponent.
Changed¶
-
AppContainer.build(redsun.containers.container) logs a presenter or a view that fails to build and carries on, as it already did for a device. The build returns, and the component is absent frompresentersorviews. -
AppContainer.connect(redsun.containers.container) returnsConnection | None. Either end belonging to a component that failed to build is logged atWARNINGand connects nothing, so the rest ofwireruns. Adeclare_*attribute of such a component reads back as a stand-in for the length of that build, and naming a port a built component does not have still raisesAttributeError. -
The line closing a build counts what was built against what was declared, and names what is missing. It is logged at
WARNINGrather thanINFOwhen anything failed to build:
Container built: 3/4 devices, 2/2 presenters, 4/5 views
Not built: bad_camera (device), log_panel (view)
-
A
wiringrule naming a component that failed to build is logged atWARNINGand skipped, and the rules around it connect. A rule naming a component that was never declared, one naming a port a built component does not expose, a signature mismatch and a malformed rule all still raise. -
AppContainer.shutdown(redsun.containers.container) releases every device, presenter and view the container built.devices,presentersandviewsraise until the nextbuild(), and adeclare_*attribute read on a shut-down container gives the declaration rather than the built object. Take a reference before the shutdown to keep using a component:
-
AppContainer.shutdownruns as named phases, each overridable by a subclass:_disconnect,_shutdown_presenters,_shutdown_hooks,_release_componentsand_destroy._destroytakes what_release_componentsreturned and does nothing by default; a toolkit overrides it to end objects that releasing does not end. -
QtAppContainer(redsun.qt) closes and destroys the widgets the container built and the main window, rather than only releasing them. A view read before the shutdown is left wrapping a destroyed widget and raisesRuntimeErroron use; presenters are unaffected. -
AppContainerbuilds its own components even when another container of the same class was built before it. The two no longer share instances.
Fixed¶
AppContainer.devices,AppContainer.presentersandAppContainer.views(redsun.containers.container) return the components the container built. A device whose build failed is absent fromdevices, where reading the mapping raisedRuntimeErrorbefore:
class App(AppContainer):
ok = declare_device(MyMotor, egu="mm")
bad = declare_device(BrokenMotor)
set(App().build().devices) # {"ok"}
- A wiring report could name a component after a different, released one.
VirtualContainer(redsun.virtual) resolves component names by identity rather than byid(), and forgets the built components at shutdown.
0.12.0 - 29-08-2026¶
Added¶
WrapsBuild(redsun.containers._hooks) andQtWrapsBuild(redsun.qt) - theduring_buildhook point, which surrounds the whole build.during_buildreturns a context manager entered before the first component is built and left once the window is shown; what it yields is called with the name of each build step as it starts.
class Splash:
@contextmanager
def during_build(self, app: QApplication) -> Generator[Callable[[str], None]]:
screen = QSplashScreen(QPixmap("logo.png"))
screen.show()
try:
yield screen.showMessage
finally:
screen.close()
AppContainer.BUILD_STEPS- the step namesbuildannounces, in order, so a progress display sizes itself from the framework rather than from a count of its own.
The steps reported are virtual container, devices, presenters, views,
providers, wiring and injection. The span opens on
QtAppContainer.run, not on build, and closes when the build raises.
run processes events once after showing the main window and before leaving
the span, so the window has painted by the time a splash is dismissed. A
provider serving configure_main_view as well holds the window and can hand
over with QSplashScreen.finish instead of close.
-
set_level(redsun.log) - sets the level of theredsunlogger. Takes aloggingconstant or a level name, aslogging.Logger.setLeveldoes; a name is matched without regard to case. -
add_handlerandremove_handler(redsun.log) - install and uninstall a destination for theredsunlogger's records. A handler carrying no formatter of its own is given the one every other destination writes through.
from redsun.log import add_handler, remove_handler
handler = MyHandler()
add_handler(handler)
...
remove_handler(handler)
log_level- a keyword onAppContainer.__init__and onAppContainer.from_config, giving the level the session runs its logger at. The logger is left as it is when it is not given.
configaccepts several YAML files, layered in the order given, and a container class reads what its bases named before its own. A file common to several sessions sits under the one particular to each.
class InstrumentApp(QtAppContainer, config="common.yaml"):
ui = declare_view(MyView, from_config="ui")
class Simulation(InstrumentApp, config="simulation.yaml"): ...
class Instrument(InstrumentApp, config="instrument.yaml"): ...
AppContainer._config_pathsreports those files in the order they layer, andAppContainer._component_fieldsrecords thedeclare_*fields a container and its bases declared.
Changed¶
- The
redsunlogger starts atINFOrather thanDEBUG, and is configured withloggingcalls rather than adictConfigmapping.redsun.log.config,redsun.log.InfoFilterandredsun.log.DebugFilterare gone: the two stream handlers they split records between wrote to onesys.stdoutthrough one formatter, which is now a single handler installed withadd_handler. AppContainerdeclares no hook points. Every point belongs to a toolkit, soQtAppContainerdeclares all four -create_application,configure_application,during_buildandconfigure_main_view- and ahookssection naming a point on a plainAppContaineris refused.- A hook never changes what the container builds or the order it builds it in.
See Toolkit hook points.
- A
declare_*field withfrom_configis resolved against the configuration of each container class that inherits it, rather than only the one that declared it. A base class can therefore carry the declarations two sessions share while each subclass reads its own files. - A subclass naming
configadds to the files its bases named instead of replacing them. - Configuration files merge as mappings, recursively: a key present in two files is taken from the later one unless both values are mappings, which merge in turn. Lists and scalars are replaced, not combined.
- The
devices,presentersandviewssections merge by component name, but a component named in a later file is taken from that file whole. A component entry is a constructor's keyword arguments, so one file owns all of them. - The keys
AppConfigrequires are checked against the merged configuration rather than against each file, so a file layered under another may carry a fragment. schema_versionandfrontendmust agree across layered files. They name what kind of session this is rather than what it contains, so a later file giving a different value raisesValueErrorinstead of overriding. Every other key,sessionincluded, is taken from the later file.- A container reading more than one configuration file logs them at debug level, in the order they layer, and logs each component an upper file takes from a lower one.
- A container inheriting from more than one base reads the files every base named, rather than only those of the first in the method resolution order. A file reached twice through the hierarchy is read once.
- A configuration section written with nothing under it -
presenters:and no entries - is read as an empty section rather than raisingAttributeError. - Declaring a
from_configfield on a container class with noconfigfile no longer raises at class creation; theTypeErroris raised when such a container is constructed, and names every field that asked for a section. A base class exists to be subclassed, and the subclass is whereconfigis named.
See Inherited and layered component configuration.
-
A required plan parameter annotated with a sequence of a non-device type -
Sequence[int],list[str]- no longer raisesUnresolvableAnnotationError. The Qt view builds a list editor for it; the check that runs before the view exists did not know that, and skipped the plan. -
Bump
ophyd-asyncto 0.21.2. - Bump
acquire-zarrto 0.9.0.
Fixed¶
- A plan with a required
boolparameter no longer crashes the Qt parameter form withTypeError: setChecked(...) argument 1 has unexpected type 'NoneType'. A parameter with no default is now given magicgui'sUndefinedrather thanNone.
Removed¶
AppContainer.phases,AppContainer.register_phaseandAppContainer.unregister_phase- the build sequence is a straight-line body again and cannot be added to.AppContainer.sig_phase_complete- aduring_buildprovider is given a reporter instead. It was the onlypsygnalSignalonAppContainer, so__weakref__leaves its__slots__.ConfiguresBuild,ConfiguresSession,AppConfiguresBuildandAppConfiguresSession(redsun.containers) - theconfigure_buildandconfigure_sessionhook points are gone with the registry and the after-the-build moment.
0.11.2 - 28-08-2026¶
Added¶
- Container hooks - an object a session installs on its application container to adjust the application as a whole. Each hook point is named by the method it calls, and takes one provider.
Providers are named in a configuration file by dotted path, under the point
they serve, with their constructor arguments under kwargs:
or declared on a container class with declare_hook:
A subclass inherits the points its bases declare. One provider serves several points when it is the same object at each: the same instance in Python, a YAML anchor and its alias in a file.
HookError is raised for an entry that does not resolve, a key that is not a
hook point the container calls, a provider that does not implement the
protocol its point calls, a point named both on the container class and in
the configuration, and two separate entries naming one provider with the same
keys. The hook points below are listed in the order a session reaches them.
See Container hooks and the build phase registry.
declare_hook(redsun.containers) - declares a hook provider on a container class, at the point the attribute names. Takes a class with keyword arguments, or a provider already built.
QtCreatesApplication(redsun.qt) - supplies theQApplicationthe session runs on. Called only when noQApplicationis running yet.
class BrandedApplication:
def create_application(self, argv: list[str]) -> QApplication:
return QApplication(argv)
QtConfiguresApplication(redsun.qt) - adjusts theQApplicationbefore the build constructs any view.
ConfiguresBuild(redsun.containers) - adjusts the build sequence before any phase of it runs. The only point at whichregister_phaseandunregister_phaseare legal.
class Calibration:
def configure_build(self, container: AppContainer) -> None:
container.register_phase("calibrate", self._run, after="injection")
ConfiguresSession(redsun.containers) - runs after the last build phase, withis_builtalready set, sodevices,presentersandviewsare readable.
class Autostart:
def configure_session(self, container: AppContainer) -> None:
self._log(container.devices, container.presenters, container.views)
QtConfiguresMainView(redsun.qt) - adjusts the main window after it is built and before it is shown. Bound toQMainWindow, not to the window class the container builds.
HasShutdown(redsun.virtual) - now called on hooks as well as presenters. Hooks are torn down in reverse order, after the presenters, once each however many points a provider serves; a failing teardown is logged and does not block the rest. The container then restores the phase sequence it captured before the hooks ran.
-
The three toolkit hook points are one generic protocol each (
CreatesApplication,ConfiguresApplication,ConfiguresMainView), aliased per toolkit. -
AppConfiguresBuildandAppConfiguresSession(redsun.containers) -
ConfiguresBuildandConfiguresSessionbound toAppContainer. Both protocols are parameterised on the container they act against, so a container implementation supplies its own aliases. -
AppContainer.phases,AppContainer.register_phase(name, phase, after=...)andAppContainer.unregister_phase(name)- the build sequence as a registry a caller can add to.afteris required;unregister_phaserefuses the built-in phases. Both are legal only beforebuild.
AppContainer.sig_phase_complete, emitted with the name of each build phase as it finishes.
class Splash:
def configure_build(self, container: AppContainer) -> None:
container.sig_phase_complete.connect(self._show)
def _show(self, phase: str) -> None: ...
-
__weakref__toAppContainer.__slots__, required by any__slots__class owning apsygnalSignal. -
QtAppContainer._ensure_main_view, so the main window is built and configured once whether reached throughrunor directly.
0.11.1 - 25-08-2026¶
Changed¶
- A presenter or view that fails its protocol check at build time now reports
every member it is missing, instead of naming the members a correct component
would have.
TypeErroris still raised for exactly the same components. - A plugin that cannot be loaded into a manifest group now reports what that group requires, instead of "does not implement any known protocol".
create_plan_specresolves each annotation on its own instead of resolving the whole signature at once. An annotation naming something unavailable at runtime, such as a type imported only underTYPE_CHECKING, now raisesUnresolvableAnnotationErrornaming the plan and the parameter, where it previously raisedNameErrorfromtyping. Such a plan is still rejected: callers that already handleUnresolvableAnnotationErrorcan skip it instead of failing the surrounding build.
0.11.0 01-08-2026¶
Added¶
slot(redsun.virtual) - marks a method as connectable, making its name and signature part of a component's public surface. Only a marked method can be connected to a signal.@slot(name=...)sets the port name used in configuration;@slot(thread=...)overrides the thread affinity declared by the class.AppContainer.wire()- override to declare the connections of an application. It runs afterregister_providersand beforeinject_dependencies; component attributes resolve to their built instances while it runs, so a connection reads asself.connect(self.det_ctrl.sig_new_data, self.img_widget.update_layers).AppContainer.connect()andVirtualContainer.connect()- connect a signal to a slot, applying the thread affinity of the slot or its class and recording the link. A slot that is not marked, or whose signaturepsygnalrejects, raisesWiringErrornaming both ports.VirtualContainer.connectionsandVirtualContainer.disconnect_all()- the recorded wiring graph and its teardown.AppContainer.shutdown()now disconnects everything it connected.ports()andPorts(redsun.virtual) - the signals and slots a component exposes, by port name.SignalGroupmembers appear under the member name.- A
wiringsection in the configuration file, listingfrom/toport paths (component.port) for a session that has no container class to override. Applied afterwire(), through the sameconnect. VirtualContainer.connect_paths()- the string form ofconnect, used by that section. A malformed path, an unbuilt component, or an unknown port raisesWiringErrorlisting what does exist.ConnectionandWiringError(redsun.virtual).VirtualContainer.provide(),require()andtry_require()- share an object under a typed key instead of a dynamic attribute.requireraisesKeyErrorwhen nothing bound the key;try_requirereturnsNone, which is how an optional collaborator is expressed.ProviderKey(redsun.virtual) - the type of such a key, adependency_injector.providers.Dependency[T].instance_of=is enforced byprovide, so a wrong value is blamed where it is supplied.PATH_PROVIDER(redsun.storage) - the key for the session path provider owned byStoragePresenter.VirtualContainer.subscribe()andVirtualContainer.subscriptions- observe anophyd-asyncdevice signal from a marked slot. The reading is marshalled throughpsygnal, sothreadbehaves as it does forconnect, and the subscription is released bydisconnect_all(). Previously a component had to callsubscribe_readingitself, from inside a coroutine, with no way to set a thread affinity and nothing tracking the release.Subscription(redsun.virtual) - the record of one, rendered assource ~> consumer.port.SlotThread(redsun.virtual) - the type of a thread affinity, so a component can annotate its__redsun_slot_thread__declaration.VirtualContainer.unconnectedandUnconnected(redsun.virtual) - the ports of the built components that no connection or subscription reaches, ascomponent.portpaths. A misspelled port fails at build; a connection that was never written fails nowhere, and this is what finds it. Falsy when everything is reached, so a script can assert on it.StorageView(redsun.view.qt.builtins) - a Qt widget showing and editing the base directory of the provider bound toPATH_PROVIDER. Without aStoragePresenterin the application it degrades to a read-only placeholder. Available from a configuration file asplugin_name: redsun,plugin_id: storageunderviews, which the shipped manifest now declares.
Changed¶
-
QtViewdeclares__redsun_slot_thread__: ClassVar[SlotThread] = "main", so every slot on a Qt view is delivered on the main thread unless the slot or the connection says otherwise. Connections that passedthread="main"explicitly still work and are now redundant. -
declare_device(),declare_presenter()anddeclare_view()return the class they are given instead ofAny, so a component attribute is typed as its component. A connection inwire()that names a port the class does not have is now a type error, where before it was only a build failure. No container needs changing: existing declarations become checked as they are. StoragePresenterbinds its provider withcontainer.provide(PATH_PROVIDER, ...). Breaking: the dynamic attribute it used to set is gone; read the provider withcontainer.require(PATH_PROVIDER)instead ofcontainer.path_provider().StoragePresenterexposesset_planandreset_planas slots instead of discoveringsig_pre_launch_notifyandsig_plan_doneby name ininject_dependencies. Breaking: an application that relied on that discovery must now connect them, inwire()or in thewiring:section:
self.connect(self.acquisition.sig_pre_launch_notify, self.storage.set_plan)
self.connect(self.acquisition.sig_plan_done, self.storage.reset_plan)
Which signals announce a plan is the application's knowledge; the presenter no longer guesses it from a name, and a misspelled one now fails instead of silently connecting nothing.
find_signals and hand-written inject_dependencies are otherwise
unaffected.
- redsun.aio.set_async_backend() - installs CulsansAsyncioBackend as psygnal's
active async backend, so coroutines connected to a signal are dispatched onto the
shared event loop from any thread. Idempotent; raises if a different backend is
already active. Tear it down with psygnal's clear_async_backend().
QtAppContainer calls it in build() and clears it in shutdown() (ADR 0005).
- CulsansAsyncioBackend and AwaitableEvent (redsun.aio) - the backend itself and
the resettable, awaitable event it reports running through. Exceptions raised by a
dispatched slot are logged on the redsun logger instead of being discarded.
Changed (breaking)¶
get_shared_loop()is no longer re-exported fromredsun.engine; import it fromredsun.aio, where it is defined. It is the only piece of the async runtime intended for use outside the application container, alongsiderun_coro().
0.10.0 25-07-2026¶
Added¶
get_shared_loop()(redsun.engine) - returns the singleasyncioevent loop created at module import time.AppContainer.connect_devices(mock=False)- connects all registeredophyd-asyncdevices via their async connect lifecycle. Call afterbuild(). Passmock=Trueto skip hardware communication in tests.FrameSink,StoreStateError, and the process-wide storage registry (register_storage,get_storage,reset_group,clear_registry).culsans(>=0.11.0) as a runtime dependency.redsun.presenter.builtins- built-in, reusable presenter components. First entry:StoragePresenter(ported from redsun-mimir'sFileStoragePresenter), which owns theSessionPathProvider, exposes it on the virtual container as thepath_providerDI provider, and wires plan names fromsig_pre_launch_notify/sig_plan_done.redsun.pluginsentry point:redsunships its own plugin manifest (plugins.yaml), so built-in components resolve from configuration files through the same discovery path as external plugins (plugin_name: redsun,plugin_id: storage).BaseStorage.path_providerread-only property.find_signalsaccepts an optionalownerkeyword to scope the lookup to one component's signal cache (ADR 0004).SinkFactory,StorageIO,OpenStore, andPathSignalsare exported fromredsun.storage- the backend protocols are part of the public contract.benchmarks/-acquire-zarrdual-load benchmark (live view viabps.monitor+ disk storage, two detectors, inline processing callback). Shipped in the sdist only, never in wheels, not collected bypytest.- The tutorial on writing a custom storage backend.
Changed (breaking)¶
redsun.storagerewritten per ADR 0002:BaseStorage.sink()returns aFrameSink(culsans-backed) usable from async device logics and sync document callbacks;open()/close()are explicit and idempotent.- Removed
StorageStateMachine,StorageState,InvalidStoreState, and theFrameSenderasync-generator API.StoreStateErrorreplacesInvalidStoreState. - Removed
redsun.device.DeviceMap-ophyd-asyncnow shipsDeviceMapas a built-in; import it fromophyd_async.coreinstead (downstream consumers such as redsun-mimir should migrate on their next refactor). - Signal naming convention:
sig_snake_casereplacessigCamelCase(ADR 0004).StoragePresenterwiressig_pre_launch_notify/sig_plan_done;DescriptorTreeView.sig_property_changedrenamed. - Presenter/view protocols reworked for sound structural subtyping (ADR
0003):
PPresenter.name/devicesandPView.nameare read-only property members; thePresenter/ViewABCs no longer inherit the protocols; validation is a dual gate - constructor positional shape ((name, devices)/(name,)) checked viainspectat declaration/discovery, protocol compliance validated on built instances (raisingTypeError) - replacing the class-level attribute screen;AppContainer.presentersand.viewsare typeddict[str, PPresenter]/dict[str, PView].
Changed¶
- Custom device layer removed,
redsun.devicenow re-exportsophyd-asyncprimitives directly. Removed:PDevice,HasChildren,AttrR,AttrRW,AttrW,AttrT,SoftAttrR,SoftAttrRW,SoftAttrT,AcquisitionController,DataWriter,ControllableDataWriter,TriggerType,PrepareInfo. Use theirophyd-asyncequivalents (Device,StandardReadable,SignalR/RW/W/X,soft_signal_rw,soft_signal_r_and_setter,DetectorController,DetectorWriter,TriggerInfo,DetectorTrigger). device(),presenter(),view()field specifiers renamed todeclare_device(),declare_presenter(),declare_view()for clarity. Update all container subclasses and imports accordingly.AppContainerMetametaclass replaced with__init_subclass__for container subclass registration.- Dropped
beartypeas a runtime dependency. - Updated CI tag pattern to support release candidates (e.g.
v0.10.0rc0). - Re-enabled CI after the test-suite rewrite: the cross-platform test matrix
and Codecov upload run again, and docs deployment / package build depend on
green tests once more. CI
mypynow uses the config-driven invocation (tests and benchmarks in scope) withQT_APIpinning the Qt binding per matrix leg, andruffchecks the whole repository instead ofsrc/redsunonly.
Removed¶
- Removed
attrsfrom dev dependencies - drop support for it in favor ofophyd-async. - Removed unused utilities:
redsun.utils.resolve_sync_or_asyncandredsun.utils.descriptors.make_key/make_descriptor/make_reading- descriptors and readings come fromophyd-asyncsignal backends; theparse_key/parse_map_keyhelpers remain.
0.9.1 - 06-03-2026¶
- Moved documentation dependencies to separate group
- Added support for
booleandtype descriptor - Updated lockfile
0.9.0 - 27-02-2026¶
Added¶
- Migrated code from
redsun-mimirto here - In particular the whole plan specification and action system
- Some things still require additional tests, although have been empirically tested in
redsun-mimir DeviceSequenceEdit: newValueWidgetsubclass renderingSequence[PDevice]andSet[PDevice]parameters as a checkbox list with a live selection count label.PlanWidget.device_widgets: exposes device parameter widgets for external validation.PlanWidget.params_widget: singleQWidgetwrapping the Devices and Parameters group boxes; disabled atomically during plan execution so all inputs lock without affecting run/stop/pause buttons.Set[PDevice]/AbstractSet[PDevice]annotation support in plan spec:isdevicesetpredicate and_handle_device_sethandler;resolve_argumentscoerces toset()for set-typed params.HasWriterprotocol expressing the ability of a device to encapsulate a writer.SessionPathProviderwith automatic run-number increment, replacingAutoIncrementFileProvider.- Metadata registry on
Writer; metadata collected atpreparetime is written immediately after stream open. clear_sourcesmechanism for presenters to explicitly clear writer sources after a plan finishes.groupparameter on path providers for sub-group addressing within a Zarr store.
Changed¶
- Storage layer migrated to per-device
Writerinstances identified by URI (singleton viaget()). - Device preparation migrated from
StorageInfo/StorageConfigdict-based API toPrepareInfo. make_writersignature updated to(uri, mimetype).- Shareable plan-spec and widget infrastructure migrated from redsun-mimir into the SDK.
create_plan_widgetnow splits device and scalar parameters into separate "Devices" and "Parameters" group boxes.- Widget factory predicates now match on annotation shape rather than
choices is not None; empty-choices case produces a valid empty widget instead of raisingRuntimeError. _try_factory_entrynow only swallows predicate errors; factory crashes propagate immediately.is_device_setremoved fromParamDescription; set coercion derived from annotation directly viaisdeviceset(p.annotation), symmetric with howisdevicesequencewas already handled.
0.8.2 - 23-02-2026¶
Changed¶
- Drop the
StaticandUUIDfilename providers in favor ofAutoIncrementas default - Will be reintroduced at a later date when storage API is stabilized
Fixed¶
- Fixed broken links in changelog
- Store the suffix of a
FilenameProvideror it gets lost - Convert URI to standard path for
acquire-zarrbackend
Added¶
- Added some helper utilities for making descriptor/reading keys following canonical convention
0.8.0 - 22-02-2026¶
Changed¶
- Migrated
sunflarecodebase toredsun.sunflarewill be archived.
0.7.2 - 22-02-2026¶
Changed¶
- Merged SDK (formerly
sunflare) intoredsun - Migrated the HasStorage protocol to toolkit
Fixed¶
- Fixed path lookup for storage
0.7.0 - 21-02-2026¶
Added¶
- Added initial support for opt-in storage capacities for devices via descriptor protocol
- Currently supporting only Zarr V3 format via
acquire-zarr
0.6.1 - 20-02-2026¶
Fixed¶
- Allow multiple widgets to be stacked in the center via
QTabWidgetforQtAppContainer - Fix the attribute look-up in loop construction to get the
view_positionattribute ofPView
0.6.0 - 20-02-2026¶
Added¶
- Added
device(),presenter(),view()typed field specifiers for declarative component registration
Changed¶
IsProvider.register_providers()now runs over both presenters and viewsIsInjectable.inject_dependencies()now runs over both presenters and views- Refactored build loop in component construction, provider registration and dependency injection
_ComponentBase: alias slot removed; name fully resolved at metaclass time_PresenterComponent.build(): removed unused container: VirtualContainer parameter- All
_*Component.build()methods use self.name directly - Changed plugin manifest format: from
{ class: "module:Type" }to flat"module:Type"string - Updated documentation
Removed¶
- Removed
component()catch-all field declarator in favor of layer-specific functions
0.5.6 - 18-02-2026¶
Fixed¶
AppContainer.build()now callsconnect_to_virtual()on allVirtualAwareview components after all components are fully constructed, symmetrically with the existing presenter loop. Previously, views were connected only via aQtMainViewdelegator called fromQtAppContainer.run(), meaning the wiring was Qt-specific and bypassed the base build phase entirely.- Removed the now-redundant
connect_to_virtual()delegator fromQtMainViewand the explicit call to it inQtAppContainer.run(). - Fixed a spurious warning when a
from_configkey exists in the YAML but has no kwargs (bare key with null value, e.g.camera2:with nothing after it). Previouslydict.get()returnedNonefor both a missing key and a null value, making them indistinguishable. A sentinel is now used so only a genuinely absent key triggers the warning; a present-but-empty section is silently normalised to{}.
Added¶
redsun.qtpublic namespace exposingQtAppContainerfor use in explicit, developer-written application configurations:- Clarified documentation
0.5.4 - 18-02-2026¶
Fixed¶
- Relaxed the
component()overloads: all three (layer="device",layer="presenter",layer="view") now accepttypeinstead oftype[Device],type[Presenter],type[View]. This fixesmypyerrors for classes built from protocol mixins that do not inherit from thesunflarebase classes directly.
0.5.3 - 18-02-2026¶
Warning
This release was yanked from PyPI due to a broken distribution
Added¶
AppContainerandcomponentare now importable directly from the top-levelredsunpackage:
Changed¶
component()now takes the component class as its first positional argument:RedSunConfigremoved from the public API; it is an internalTypedDictused only for YAML configuration validation.
0.5.0 - 17-02-2026¶
Changed¶
- Fully refactor the package to go towards a containerization approach
- Declare applications as containers, list relevant components as fields of a class
- Provide support also for building from a configuration file as before
- Upgrade to
sunflare>=0.9.0 - Move the
FrontendTypesandViewPositionTypesfromsunflaretoredsun - They're part of the overall configuration and should not concern the core package
- Revamped documentation with more comprehensive information
0.4.0 - 15-12-2025¶
Changed¶
- Apply a more strict check on imported plugins
- Add support for 3.13 (simply declared on PyPI and tested via CI)
- Upgrade to
sunflare>=0.7.0
0.3.0 - 04-07-2025¶
Changed¶
- Upgraded to
sunflare>=0.6.1 - Switch to
uv - Drop support for Python 3.9
0.2.0 - 03-03-2025¶
Changed¶
- Reworked the plugin system
- The approach now loosely follows the
naparimanifest, where plugins are to be published via ayamlconfiguration file in the root folder of the plugin package, specifiying where the classes have to be imported. - The manifest is taken as the actual entry point of a plugin, which will be used to redirect to the actual imports which is executed via the standard library
importlib. - Added additional coverage for the
factorymodule. - Bumped
sunflareversion to`sunflare`>=0.5.0, which implements the above changes at toolkit level
0.1.0 - 22-02-2025¶
Added¶
- Initial release on PyPI