spacr.omero

OMERO import / export — a spaCR project out of a server, and results back in.

A large share of imaging labs never see a filesystem. Their images live in an OMERO server, they are addressed by id, and the only way anyone looks at a result is the right-hand panel in OMERO.web. spaCR, by contrast, is built around a flat folder of Yokogawa-named TIFFs. This module is the bridge, in both directions, and it is deliberately two directions rather than one: a one-way copy out of OMERO leaves the answers stranded in a folder nobody on the microscope side will ever open.

What it does

Import. import_dataset() and import_plate() take an OMERO Dataset or Plate id and write a spaCR source folder:

plate1_A01_T0001F001L01A01Z01C01.tif plate1_A01_T0001F001L01A01Z01C02.tif plate1_A01_T0001F002L01A01Z01C01.tif …

That is spacr.convert.target_name() verbatim — the exact shape spacr.utils._get_regex('cellvoyager', 'tif') parses — so the output folder is handed straight to Mask/Measure with metadata_type='cellvoyager' and nothing else. No new layout is invented here. The one thing that must survive the trip is the plate geometry, because spaCR keys every measurement, every plot and every well-level statistic off plateID / rowID / columnID / fieldID; well_position() is where OMERO’s 0-based (row, column) becomes spaCR’s ('r1', 'c1') and the well name A01.

Export. export_map_annotation(), export_file_annotation() and export_tag_annotation() push spaCR’s own output back onto the OMERO objects it came from: a key/value MapAnnotation per image or per well (what OMERO.web actually renders), a FileAnnotation for the results CSV or the figure PDF, and a TagAnnotation for a categorical verdict.

Why the logic is not in the adapter

Everything that is easy to get wrong here is arithmetic and string formatting, not RPC: the row/column mapping, the filename, the id parsing, the float formatting, the decision to replace rather than append. All of it lives in pure functions over plain data — well_position(), plane_filename(), parse_object_ref(), format_map_value(), measurement_pairs(), plan_annotation() — and the BlitzGateway calls are confined to a thin layer that only walks containers and moves bytes. That is why this module has a real test suite on a machine with no OMERO server and no omero-py installed: the fake gateway in tests/test_omero.py implements about a dozen methods, and everything worth testing is reachable through it.

Every adapter function takes gateway as its first argument. There is no module-level connection and no implicit session — a caller that wants one calls connect(), and a test passes its own object.

The well mapping, stated

OMERO’s WellWrapper.getRow() and .getColumn() are 0-based. well_position() adds one and hands the result to spacr.schema.well_id(), which is spaCR’s own definition of a well name. Consequences worth writing down because they are the ones that get fumbled:

  • row 0 -> A, row 7 -> H (a 96-well plate), row 15 -> P (384).

  • row 25 -> Z.

  • row 26 -> AA, not [ (which is what chr(65 + 26) gives) and not an IndexError (which is what string.ascii_uppercase[26] gives). This is bijective base 26, and it is not a hypothetical: a 1536-well plate has 32 rows and runs A..Z, AA..AF. Column 47 -> 48, for the same plate, so nothing here caps the column at 24 either.

  • the column is zero padded to two digits (A01, never A1) because spaCR’s strict Yokogawa regex is [A-Z]\d{2}.

Sizes and units

ImageWrapper.getPixelSizeX() returns a length object with a unit, not a number of micrometres. This module never assumes µm: pixel_size_from() carries the value and the unit symbol together in a PixelSize and writes both into the import sidecar. A pixel size recorded in nm that is read back as µm is a 1000x error in every area in the database and it is silent.

What is written alongside the images

Two sidecars, in the destination folder, both of which spaCR itself is happy to ignore:

omero_import.csv

one row per plane: the target filename, the OMERO image id and name, the spaCR keys (plateID/rowID/columnID/fieldID/prc), the channel index and its OMERO name, z/t, the pixel size and its unit, and where the well came from (see well_source below).

omero_import.json

one object per import: server host and port, container kind/id/name, plate token, counts, channel names, pixel size, the spaCR version and a UTC timestamp.

Neither ever contains a password or a session key. That is asserted by a test, along with the fact that OmeroConnection will not print one either.

Not downloading a 100 GB plate to answer “what is in it”

inspect_container() reports the image count, the wells, the dimensions and the channel names without ever calling getPlane — a fact the test suite pins by counting calls on the fake gateway. The importers additionally take limit (stop after N OMERO images) and dry_run (build the complete plan, list every filename that would be written, touch no pixels).

Replace or append, and what is never done

Annotations are written under a spaCR-owned namespace (NAMESPACE_ROOT), which is what makes a second run able to find its own previous output instead of piling up a fifth copy of the same key/value table.

The default is REPLACE, implemented as update-in-place, and it is the safe option rather than a compromise: an existing MapAnnotation whose namespace is exactly spaCR’s has its value overwritten. Nothing is created, nothing is unlinked, nothing is deleted, and no annotation outside spaCR’s namespaces is so much as read for its value. APPEND is available and explicit for anyone who wants a history of runs on the object.

Three consequences are stated rather than hidden:

  • This module never deletes anything. There is no call to deleteObjects in it, under any mode, for any object. If a previous APPEND run left three copies, REPLACE updates the oldest and reports the rest in AnnotationResult.duplicates — removing them is a decision for a human with the OMERO.web UI, not for an importer.

  • FileAnnotations always append. The bytes of an OriginalFile cannot be rewritten in place through the gateway, and the alternative — delete the old attachment — is exactly the destructive behaviour ruled out above. The namespace plus the timestamp in the description identify the newest.

  • TagAnnotations are never edited. In OMERO a tag is a shared object; renaming the tag linked to this image renames it on every other object in the group that carries it. So export_tag_annotation() links a tag when the verdict is new, does nothing when the identical verdict is already linked, and reports the previous tag when the verdict has changed — it never unlinks and never renames.

Connection settings and secrets

connection_settings() reads arguments first and the environment second: OMERO_HOST, OMERO_PORT, OMERO_USER, OMERO_PASSWORD (or OMERO_PASS), OMERO_SESSION_KEY, OMERO_GROUP, OMERO_SECURE. OmeroConnection is frozen and redacts both secrets from repr(), str() and OmeroConnection.redacted(), and no log record in this module carries one.

The optional dependency

omero-py is an extra: pip install "spacr[omero]". It is deliberately not part of spacr[all], because it depends on zeroc-ice, a compiled C++ Ice runtime whose wheels lag Python releases and which otherwise needs a C++ toolchain (see the comment beside the extra in setup.py).

So import spacr.omero must work without it, and it does: the import is function-local, behind require_omero(), and a missing install produces one actionable sentence naming pip install "spacr[omero]" rather than a ModuleNotFoundError six frames deep inside Ice’s own import machinery. This follows spacr.qt._QT_MISSING_MESSAGE and spacr.anndata_export.require_anndata().

Two details of that guard are worth knowing:

  • A missing Ice counts as a missing omero extra. A half-built zeroc-ice is the single most likely way this fails in the field, and No module named 'Ice' mentions neither OMERO nor spaCR. missing_omero_message() says what happened.

  • This module is called spacr/omero.py and does not shadow the third-party omero package. Absolute imports have been the default since Python 3, so a module inside the spacr package that asks for omero gets the top-level distribution, not its own sibling. That is verified rather than assumed — see tests/test_omero.py::test_spacr_omero_does_not_shadow_the_third_party_package, which puts a decoy omero package on sys.path and checks which one arrives — because a self-import here would be silent and would look like a broken OMERO install.

The import is written as importlib.import_module(OMERO_GATEWAY_MODULE) rather than as a literal import omero.gateway for that same reason: the one place the name is resolved is a named constant next to the guard that checks what came back, instead of a bare statement in a file of the same name. The cost is that tests/test_declared_dependencies_match_imports.py — which reads import statements out of the AST — cannot see it, exactly as it cannot see umap. That blind spot is pinned from the other side by tests/test_omero.py::test_omero_py_is_reached_through_a_string_literal_and_must_not_be_removed, so the next dependency census that reads “omero-py: unused” finds the answer instead of deleting the extra.

Attributes

Exceptions

OmeroConnectionError

Connection settings are incomplete, or the server refused the login.

OmeroContainerError

The id resolved to nothing, or to a container with nothing usable in it.

OmeroError

Base class for every refusal this module makes.

OmeroExtraMissing

omero-py (or the Ice runtime under it) is not installed.

OmeroIdError

An object id could not be read, or names the wrong kind of object.

OmeroWellError

A well's row/column indices are not a position on a plate.

Classes

AnnotationPlan

What an export is about to do, decided before anything is touched.

AnnotationResult

What one annotation write did.

ContainerListing

The answer to "what is in this container", with no pixels fetched.

ExportResult

What a multi-target export did.

ImageInfo

What is known about one OMERO image without reading a single pixel.

ImportResult

What an import did, or (with dry_run) what it would have done.

OmeroConnection

Everything needed to open a session, with the secrets kept out of sight.

OmeroRef

A parsed reference to one OMERO object.

PixelSize

A physical pixel size together with the unit it was recorded in.

PlanePlan

One 2-D plane that would be, or was, written.

WellPosition

One well, in both vocabularies at once.

Functions

connect(→ Any)

Open a session and return the connected gateway.

connection_settings(→ OmeroConnection)

Build an OmeroConnection from arguments, then the environment.

export_file_annotation(→ AnnotationResult)

Attach a results CSV or a figure PDF to an OMERO object.

export_map_annotation(→ AnnotationResult)

Write spaCR measurements onto an OMERO object as key/value pairs.

export_plate_summaries(→ ExportResult)

Write one per-well MapAnnotation per well of an OMERO Plate.

export_tag_annotation(→ AnnotationResult)

Attach a categorical verdict — 'hit', 'QC fail' — as a tag.

format_map_value(→ str)

Render value as the string an OMERO map annotation stores.

have_omero(→ bool)

Report whether the omero extra can be imported, without raising.

import_container(→ ImportResult)

Import a Dataset or a Plate, dispatching on what ref says it is.

import_dataset(→ ImportResult)

Import an OMERO Dataset into a spaCR source folder.

import_plate(→ ImportResult)

Import an OMERO Plate into a spaCR source folder.

inspect_container(→ ContainerListing)

Report what a Dataset or Plate contains, without fetching pixels.

is_missing(→ bool)

Return whether value represents missing annotation data.

is_spacr_namespace(→ bool)

Report whether namespace is one spaCR owns and may write to.

list_spacr_annotations(→ Tuple[Tuple[int, ...)

Return the (id, namespace) of every spaCR annotation on target.

measurement_pairs(→ Tuple[Tuple[str, str], ...])

Project one measurement row onto the key/value pairs OMERO stores.

missing_omero_message(→ str)

Return the full "install the extra" text naming module.

omero_indices(→ Tuple[int, int])

Invert well_position(): 'A01' -> (0, 0).

parse_object_id(→ int)

Parse value to a positive OMERO id, checking the type it names.

parse_object_ref(→ OmeroRef)

Parse an OMERO object reference into a kind and a positive id.

pixel_size_from(→ PixelSize)

Read a PixelSize out of whatever getPixelSizeX() returned.

plan_annotation(→ AnnotationPlan)

Decide whether to overwrite spaCR's previous annotation or add one.

plan_tag(→ AnnotationPlan)

Decide what to do about a categorical verdict tag.

plane_filename(→ str)

Return the Yokogawa filename spaCR expects for one plane.

plate_token(→ str)

Reduce name to a plate token that survives spaCR's filename regex.

require_omero(→ Any)

Import and return omero.gateway, or raise a message worth reading.

summarise_rows(→ Dict[str, Any])

Reduce many object rows to one per-well summary, in plain Python.

well_from_image_name(→ Optional[str])

Recover a well name from an OMERO image name, or return None.

well_position(→ WellPosition)

Map OMERO's 0-based (row, column) onto spaCR's well keys.

well_summary_pairs(→ Tuple[Tuple[str, str], ...])

Summarise object rows and project them onto key/value pairs.

Module Contents

exception spacr.omero.OmeroConnectionError[source]

Bases: OmeroError

Connection settings are incomplete, or the server refused the login.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.omero.OmeroContainerError[source]

Bases: OmeroError

The id resolved to nothing, or to a container with nothing usable in it.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.omero.OmeroError[source]

Bases: ValueError

Base class for every refusal this module makes.

A ValueError rather than a bespoke hierarchy root: every one of these is “the input does not describe something I can act on”, and a caller that already handles ValueError around a conversion step keeps working.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.omero.OmeroExtraMissing[source]

Bases: ImportError

omero-py (or the Ice runtime under it) is not installed.

An ImportError subclass, so a caller already guarding with except ImportError keeps working and the actionable message — not a traceback through Ice’s import machinery — is what reaches the user.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.omero.OmeroIdError[source]

Bases: OmeroError

An object id could not be read, or names the wrong kind of object.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.omero.OmeroWellError[source]

Bases: OmeroError

A well’s row/column indices are not a position on a plate.

Initialize self. See help(type(self)) for accurate signature.

class spacr.omero.AnnotationPlan[source]

What an export is about to do, decided before anything is touched.

Parameters:
  • action – ACTION_CREATE, ACTION_UPDATE or ACTION_UNCHANGED.

  • annotation_id – the annotation to update, for ACTION_UPDATE/ACTION_UNCHANGED; None for a create.

  • namespace – the namespace the annotation lives in.

  • duplicates – other spaCR annotations already in that namespace, left untouched and reported.

  • reason – one sentence explaining the choice, for the result and the log.

class spacr.omero.AnnotationResult[source]

What one annotation write did.

Parameters:
  • action – ACTION_CREATE, ACTION_UPDATE or ACTION_UNCHANGED.

  • namespace – the namespace written.

  • annotation_id – the annotation’s id, when the server gave one.

  • n_pairs – how many key/value pairs were written (map annotations).

  • duplicates – other spaCR annotations in the same namespace, left untouched.

  • reason – the plan’s explanation, carried through.

class spacr.omero.ContainerListing[source]

The answer to “what is in this container”, with no pixels fetched.

Parameters:
  • kind – 'Dataset' or 'Plate'.

  • object_id – the container’s OMERO id.

  • name – the container’s OMERO name.

  • images – one ImageInfo per image found.

  • wells – the well names present, for a Plate; empty for a Dataset.

  • unplaced_wells – how many wells had no row/column and were skipped.

describe() → str[source]

Return a multi-line summary suitable for printing.

Returns:

the container, its images, their dimensions and channels, and the number of TIFFs an import would write.

property channels: Tuple[str, ...][source]

Return the channel names of the first image, or ().

property n_images: int[source]

Return the number of images in the container.

property n_planes: int[source]

Return how many 2-D TIFFs a full import would write.

class spacr.omero.ExportResult[source]

What a multi-target export did.

Parameters:
  • namespace – the namespace written.

  • results – one AnnotationResult per target, keyed in targets order.

  • targets – the target labels (well names, image ids) in order.

  • missing – labels that were asked for but not found on the container.

describe() → str[source]

Return a one-line summary of the export.

Returns:

counts of created, updated and unchanged annotations.

class spacr.omero.ImageInfo[source]

What is known about one OMERO image without reading a single pixel.

Parameters:
  • image_id – the OMERO image id.

  • name – the OMERO image name.

  • size_x – width in pixels.

  • size_y – height in pixels.

  • size_z – number of z-slices.

  • size_c – number of channels.

  • size_t – number of timepoints.

  • channels – the channel names OMERO holds, in channel order.

  • pixel_size_x – the physical pixel size in x, with its unit.

  • pixel_size_y – the physical pixel size in y, with its unit.

  • pixel_size_z – the z step, with its unit.

  • well – the well this image sits in, for a Plate; None in a Dataset, which has no plate geometry.

  • field_id – the 1-based field (WellSample) index, for a Plate.

property n_planes: int[source]

Return how many 2-D planes this image would import as.

class spacr.omero.ImportResult[source]

What an import did, or (with dry_run) what it would have done.

Parameters:
  • kind – 'Dataset' or 'Plate'.

  • object_id – the container’s OMERO id.

  • name – the container’s OMERO name.

  • plate – the plateID token every filename was built with.

  • dst – the destination folder.

  • planned – every plane the import covered, in write order.

  • written – the filenames actually written.

  • skipped – filenames that already existed and were left alone.

  • dry_run – whether pixels were fetched at all.

  • limited – whether limit stopped the walk before the end.

  • n_images – how many OMERO images were visited.

describe() → str[source]

Return a short human summary of the import.

Returns:

a multi-line string naming the container, the plate token, the counts and the destination.

class spacr.omero.OmeroConnection[source]

Everything needed to open a session, with the secrets kept out of sight.

Frozen, so a settings object cannot be mutated halfway through a run, and repr-suppressed, because the single most common way a password reaches a log file, a crash report or a notebook cell is somebody printing the object that holds it. redacted() is the form that is safe to write down.

Parameters:
  • host – server hostname. Required.

  • port – server port, default DEFAULT_PORT.

  • username – OMERO user name. Required for password auth, optional with a session key.

  • password – the password. Never printed.

  • session_key – an existing session uuid, used instead of a password. Never printed.

  • secure – whether to keep the connection encrypted after login. Default True.

  • group – optional OMERO group to switch into after connecting.

__repr__() → str[source]

Return a repr that names the secrets without containing them.

describe() → str[source]

Return a one-line human description, with no secret in it.

Returns:

e.g. omero.example.org:4064 as jdoe (password, secure).

redacted() → Dict[str, Any][source]

Return a plain dict of the settings, safe to log or serialise.

Both secrets are replaced by SECRET_PLACEHOLDER when present and by None when absent, so the shape of the record does not itself leak whether a password was supplied… it does say which credential was used, which is diagnostic and not a secret.

Returns:

a JSON-serialisable dict with no credential in it.

property auth_mode: str[source]

Return 'session' or 'password' — which credential is used.

A session key wins when both are present, because that is what BlitzGateway itself does with them.

class spacr.omero.OmeroRef[source]

A parsed reference to one OMERO object.

Parameters:
  • kind – the container type in OMERO’s capitalisation ('Dataset'), or None when the input was a bare number and the caller must say what it is.

  • object_id – the positive integer id.

  • text – the original input, kept for error messages.

describe() → str[source]

Return 'Dataset:123', or '123' when the kind is unknown.

Returns:

a short human-readable reference.

class spacr.omero.PixelSize[source]

A physical pixel size together with the unit it was recorded in.

Parameters:
  • value – the magnitude, or None when the server has no calibration for this image (which is common, and is not an error).

  • unit – the unit symbol as OMERO reports it — 'MICROMETER', 'µm', 'nm'. Carried verbatim; this module never converts, because a silent unit conversion is a silent factor of 1000.

__bool__() → bool[source]

Return whether a magnitude is present.

describe() → str[source]

Return '0.325 MICROMETER', or 'unknown'.

Returns:

a short human-readable pixel size.

class spacr.omero.PlanePlan[source]

One 2-D plane that would be, or was, written.

Parameters:
  • filename – the target filename (not a path — the destination folder is the caller’s).

  • image_id – the OMERO image the plane comes from.

  • image_name – that image’s OMERO name.

  • well – the well name, e.g. 'A01'.

  • position – the full WellPosition for that well.

  • field_id – 1-based field id.

  • channel – 1-based channel id.

  • channel_name – the channel’s OMERO name.

  • z – 1-based z index.

  • t – 1-based timepoint index.

  • info – the source image’s ImageInfo.

  • well_source – 'plate' (from the well’s row/column), 'name' (parsed out of the image name) or 'sequence' (assigned in listing order because nothing else said).

csv_row(plate: str) → Dict[str, Any][source]

Return this plane as a MAP_CSV_COLUMNS row.

Parameters:

plate – the plateID token used for the filenames.

Returns:

a dict keyed by MAP_CSV_COLUMNS.

class spacr.omero.WellPosition[source]

One well, in both vocabularies at once.

Parameters:
  • row_index – 1-based row (spaCR’s convention). OMERO’s row + 1.

  • column_index – 1-based column. OMERO’s column + 1.

  • row_id – spaCR’s rowID, e.g. 'r1'.

  • column_id – spaCR’s columnID, e.g. 'c1'.

  • well – the well name, e.g. 'A01' — zero padded to two column digits, which is what spaCR’s strict Yokogawa regex requires.

  • plate_format – the smallest standard plate this position fits (96, 384, 1536, …), or None when it fits none. Reported, never enforced: a partial or non-standard plate is a real thing.

prc(plate: str) → str[source]

Return spaCR’s prc well key for this position on plate.

Parameters:

plate – the plateID token.

Returns:

'plate1_r1_c1'.

spacr.omero.connect(settings: OmeroConnection | None = None, *, gateway_factory: Any | None = None, **overrides: Any) → Any[source]

Open a session and return the connected gateway.

settings may be omitted entirely, in which case one is built from **overrides and the environment through connection_settings().

The caller owns the returned object and is responsible for closing it (gateway.close()); this module deliberately does not hold a module-level connection, so nothing here can leak a session between two unrelated runs.

Parameters:
  • settings – a prepared OmeroConnection, or None.

  • gateway_factory – callable taking the settings and returning an unconnected gateway. Defaults to building a real BlitzGateway, which is the only line in this module that needs the extra.

  • overrides – passed to connection_settings() when settings is None.

Returns:

the connected gateway object.

Raises:
spacr.omero.connection_settings(host: str | None = None, *, port: int | str | None = None, username: str | None = None, password: str | None = None, session_key: str | None = None, secure: bool | str | None = None, group: str | None = None, env: Mapping[str, str] | None = None) → OmeroConnection[source]

Build an OmeroConnection from arguments, then the environment.

Arguments always win; anything left unset is looked up in env through ENV_VARS. Reading the environment matters more here than it looks: it is how a password stays out of a notebook, out of a settings CSV and out of the shell history, which is the same reason OMERO_PASSWORD exists at all.

Parameters:
  • host – server hostname; falls back to OMERO_HOST.

  • port – server port; falls back to OMERO_PORT, then DEFAULT_PORT.

  • username – falls back to OMERO_USER / OMERO_USERNAME.

  • password – falls back to OMERO_PASSWORD / OMERO_PASS.

  • session_key – falls back to OMERO_SESSION_KEY; used instead of a password when present.

  • secure – falls back to OMERO_SECURE, then True.

  • group – falls back to OMERO_GROUP.

  • env – the environment to read. Defaults to os.environ; passing a plain dict is how the tests avoid touching the real one.

Returns:

a validated, frozen OmeroConnection.

Raises:

OmeroConnectionError – when the host is missing, the port is not a usable TCP port, no credential was supplied, or a password was given with no user name to go with it.

spacr.omero.export_file_annotation(gateway: Any, target: Any, path: str | os.PathLike, *, namespace: str = NS_FILE, mimetype: str | None = None, description: str | None = None) → AnnotationResult[source]

Attach a results CSV or a figure PDF to an OMERO object.

The counterpart to the truncated MapAnnotation: the panel gets the fifty numbers worth reading, this gets the whole table.

File annotations always append, and that is the non-destructive choice rather than an oversight. The bytes of an OriginalFile cannot be rewritten through the gateway, so “replace” would have to mean delete the previous attachment — deleting evidence of an earlier run, which is exactly what this module refuses to do anywhere. Previous spaCR file annotations are reported in AnnotationResult.duplicates, and the description carries a UTC timestamp so the newest is identifiable.

Parameters:
  • gateway – a connected gateway; createFileAnnfromLocalFile is called on it.

  • target – the Dataset, Plate, Image or Well to attach to.

  • path – the local file.

  • namespace – the spaCR file namespace.

  • mimetype – overrides the type guessed from the extension.

  • description – overrides the generated description.

Returns:

an AnnotationResult with action=create.

Raises:

OmeroError – for a foreign namespace, or a path that is not a file.

spacr.omero.export_map_annotation(gateway: Any, target: Any, pairs: Sequence[Sequence[Any]], *, namespace: str = NS_MEASUREMENTS, mode: str = REPLACE, annotation_factory: Any | None = None) → AnnotationResult[source]

Write spaCR measurements onto an OMERO object as key/value pairs.

A MapAnnotation is what OMERO.web renders in the right-hand panel, which makes it the only annotation an OMERO user reliably sees. Build the pairs with measurement_pairs() (per object) or well_summary_pairs() (per well) — both already do the string conversion OMERO requires and the truncation the panel requires.

Parameters:
  • gateway – a connected gateway; used only to construct the wrapper.

  • target – the Image, Well, Dataset or Plate wrapper to annotate.

  • pairs – the key/value pairs. Values are cast to str here as a last line of defence — OMERO’s model has no numeric map value.

  • namespace – which spaCR namespace to write. Must be one spaCR owns.

  • mode – REPLACE (default: update spaCR’s own previous annotation in place) or APPEND.

  • annotation_factory – callable (wrapper_name, gateway) returning a new annotation wrapper. Defaults to the real omero.gateway.MapAnnotationWrapper; the tests pass their own.

Returns:

an AnnotationResult.

Raises:

OmeroError – for an unknown mode or a foreign namespace.

spacr.omero.export_plate_summaries(gateway: Any, ref: Any, summaries: Mapping[str, Sequence[Mapping[str, Any]]], *, namespace: str = NS_WELL_SUMMARY, mode: str = REPLACE, annotation_factory: Any | None = None, **pair_kwargs: Any) → ExportResult[source]

Write one per-well MapAnnotation per well of an OMERO Plate.

This is the half that closes the loop: spaCR measured the plate, and the numbers land back on the wells they came from, where the person at the microscope will actually see them. Wells are matched by name ('A01'), which round-trips through well_position() and omero_indices(), so the mapping used on the way in is the mapping used on the way out.

Parameters:
  • gateway – a connected gateway.

  • ref – a Plate id, 'Plate:4711', or an OMERO.web URL.

  • summaries – {well_name: [object_row, ...]}. The rows are summarised with summarise_rows(); pass a single already-summarised row as a one-element list to skip the aggregation.

  • namespace – the spaCR well-summary namespace.

  • mode – REPLACE (default) or APPEND.

  • annotation_factory – callable (wrapper_name, gateway).

  • pair_kwargs – passed to measurement_pairs().

Returns:

an ExportResult; wells named in summaries that the Plate does not have are listed in ExportResult.missing rather than raising, because a plate map with an extra control well in it is a normal thing to hand in.

Raises:
spacr.omero.export_tag_annotation(gateway: Any, target: Any, text: str, *, namespace: str = NS_TAG, mode: str = REPLACE, annotation_factory: Any | None = None) → AnnotationResult[source]

Attach a categorical verdict — 'hit', 'QC fail' — as a tag.

See plan_tag() for why a tag is never edited in place: it is a shared object, and renaming it would relabel every other object carrying it. This is idempotent for an unchanged verdict and additive (with a report) for a changed one, and it never unlinks or deletes.

Parameters:
  • gateway – a connected gateway.

  • target – the object to tag.

  • text – the verdict.

  • namespace – the spaCR verdict namespace.

  • mode – REPLACE (default) or APPEND.

  • annotation_factory – callable (wrapper_name, gateway); defaults to omero.gateway.TagAnnotationWrapper.

Returns:

an AnnotationResult.

Raises:

OmeroError – for an unknown mode, a foreign namespace, or an empty verdict (a tag with no text is invisible in OMERO.web).

spacr.omero.format_map_value(value: Any, *, float_format: str = FLOAT_FORMAT, max_chars: int = MAX_VALUE_CHARS) → str[source]

Render value as the string an OMERO map annotation stores.

OMERO map values are strings — there is no numeric type in the model — so the formatting decision is not cosmetic, it is the only thing that decides what a reader sees and what a re-parser gets back. The rules, all of them:

  • bool first, before the numeric branch, because True is an int and would otherwise render as 1. numpy.bool_ needs its own check for the same reason: it is not a bool, and it does answer __index__.

  • integers render exactly: str(int(value)), never through float_format, so a 12-digit object id does not become 1.23457e+11.

  • floats go through FLOAT_FORMAT — six significant digits, with trailing zeros dropped, so 3.0 is '3' and 1.23456789e-5 is '1.23457e-05'.

  • inf and -inf render as 'inf' / '-inf' rather than being hidden, because in spaCR they mean a ratio with a zero denominator and that is worth seeing.

  • missing values (see is_missing()) render as MISSING_TEXT. Dropping them instead is what NAN_DROP is for, at the measurement_pairs() level; here the value has to become some string.

  • anything else is str(), stripped, newlines collapsed to spaces (a newline inside a map value breaks the panel’s layout), and truncated to max_chars with a trailing ….

Parameters:
  • value – the value to render.

  • float_format – the format string used for non-integral floats.

  • max_chars – the length at which the text is truncated.

Returns:

a string, always.

spacr.omero.have_omero() → bool[source]

Report whether the omero extra can be imported, without raising.

Returns:

True when require_omero() would succeed.

spacr.omero.import_container(gateway: Any, ref: Any, dst: str | os.PathLike, *, kind: str | None = None, **kwargs: Any) → ImportResult[source]

Import a Dataset or a Plate, dispatching on what ref says it is.

Parameters:
  • gateway – a connected gateway.

  • ref – 'Plate:4711', 'Dataset:123', an OMERO.web URL, or a bare id together with kind.

  • dst – destination folder.

  • kind – 'Dataset' or 'Plate', required when ref is bare.

  • kwargs – passed to import_dataset() or import_plate().

Returns:

an ImportResult.

Raises:

OmeroIdError – when the kind is unknown, unsupported, or contradicts ref.

spacr.omero.import_dataset(gateway: Any, ref: Any, dst: str | os.PathLike, *, plate: str | None = None, limit: int | None = None, dry_run: bool = False, overwrite: bool = False, well_from_name: bool = True, settings: OmeroConnection | None = None) → ImportResult[source]

Import an OMERO Dataset into a spaCR source folder.

A Dataset is a flat bag of images with no plate geometry, so the wells have to come from somewhere. Two sources, in order, and which one was used is recorded per file in SIDECAR_CSV:

'name'

the image name contains a delimited well token (see well_from_image_name()). Turned off with well_from_name=False.

'sequence'

nothing said, so wells are handed out row-major in listing order — A01, A02, … — one per image. This is a labelling, not a claim about the physical plate, and it is what makes the images loadable by spaCR at all.

Parameters:
  • gateway – a connected gateway.

  • ref – a Dataset id, 'Dataset:123', or an OMERO.web URL.

  • dst – destination folder; created if missing.

  • plate – the plateID token. Defaults to the Dataset’s own name, run through plate_token().

  • limit – stop after this many OMERO images. The cheap way to try an import against a container you have not seen.

  • dry_run – plan everything, write the sidecars, fetch no pixels.

  • overwrite – rewrite TIFFs that already exist. Off by default, so a re-run resumes rather than redoing the download.

  • well_from_name – whether to look for a well in the image name.

  • settings – the connection these images came from, recorded (redacted) in SIDECAR_JSON.

Returns:

an ImportResult.

Raises:
  • OmeroIdError – when ref names something other than a Dataset — a Plate id passed here is refused rather than half-imported.

  • OmeroContainerError – when the id resolves to nothing or the Dataset has no images.

spacr.omero.import_plate(gateway: Any, ref: Any, dst: str | os.PathLike, *, plate: str | None = None, limit: int | None = None, dry_run: bool = False, overwrite: bool = False, settings: OmeroConnection | None = None) → ImportResult[source]

Import an OMERO Plate into a spaCR source folder.

A Plate is the case spaCR was built for: every image already knows its well, and the well knows its row and column. Those indices are carried across by well_position() and become the well name in the filename and rowID/columnID in the sidecar, so a plate map drawn in spaCR lines up with the plate map in OMERO.web.

Fields are the WellSample order within each well, 1-based, which is what OMERO means by an imaging site.

Wells with no row/column (present in the Plate but never placed) are skipped and counted, never defaulted to A01.

Parameters:
  • gateway – a connected gateway.

  • ref – a Plate id, 'Plate:4711', or an OMERO.web URL.

  • dst – destination folder; created if missing.

  • plate – the plateID token. Defaults to the Plate’s own name.

  • limit – stop after this many OMERO images.

  • dry_run – plan everything, write the sidecars, fetch no pixels.

  • overwrite – rewrite TIFFs that already exist.

  • settings – the connection, recorded (redacted) in the JSON sidecar.

Returns:

an ImportResult.

Raises:
  • OmeroIdError – when ref names something other than a Plate.

  • OmeroContainerError – when the id resolves to nothing or the Plate has no placed wells with images.

spacr.omero.inspect_container(gateway: Any, ref: Any, *, kind: str | None = None) → ContainerListing[source]

Report what a Dataset or Plate contains, without fetching pixels.

This exists so that “what is in plate 4711?” costs a metadata query rather than a 100 GB download. Nothing in this function or anything it calls touches getPrimaryPixels or getPlane, and the test suite asserts that by counting calls on the fake gateway.

Parameters:
  • gateway – a connected BlitzGateway (or anything with the same getObject surface).

  • ref – an id, a 'Plate:4711' reference, or an OMERO.web URL.

  • kind – the object type, when ref is a bare id. Defaults to the type named by ref; one of the two must say.

Returns:

a ContainerListing.

Raises:
  • OmeroIdError – for an unusable id, or when kind and ref disagree, or when neither says what the object is.

  • OmeroContainerError – when the id resolves to nothing.

spacr.omero.is_missing(value: Any) → bool[source]

Return whether value represents missing annotation data.

Missing values include None, floating-point NaN, pandas NA and NaT sentinels, and objects for which self-inequality cannot be reduced to a boolean. These values are omitted rather than serialized as literal sentinel text in an OMERO map annotation.

Parameters:

value – anything.

Returns:

True when the value is missing.

spacr.omero.is_spacr_namespace(namespace: str | None) → bool[source]

Report whether namespace is one spaCR owns and may write to.

The test is exact equality against SPACR_NAMESPACES, not a prefix match: a prefix match would claim github.com/EinarOlafsson/spacr-fork/ 1/measurements as spaCR’s own and overwrite somebody else’s annotation.

Parameters:

namespace – an annotation namespace, possibly None (OMERO’s default namespace, which spaCR never writes to).

Returns:

True when spaCR owns it.

spacr.omero.list_spacr_annotations(target: Any) → Tuple[Tuple[int, str | None], ...][source]

Return the (id, namespace) of every spaCR annotation on target.

listAnnotations() is deliberately called without a namespace filter and the filtering is done here. The filter is what decides whether an annotation gets written to, and that decision must not depend on a server honouring a query parameter: a server that ignored ns= would otherwise hand back a stranger’s annotation and this module would update it.

Parameters:

target – any OMERO object wrapper with listAnnotations().

Returns:

(id, namespace) pairs, only for namespaces in SPACR_NAMESPACES.

spacr.omero.measurement_pairs(row: Mapping[str, Any], *, columns: Sequence[str] | None = None, priority: Sequence[str] = PRIORITY_KEYS, max_pairs: int = MAX_MAP_PAIRS, nan_policy: str = NAN_KEEP, extra: Mapping[str, Any] | None = None, float_format: str = FLOAT_FORMAT) → Tuple[Tuple[str, str], ...][source]

Project one measurement row onto the key/value pairs OMERO stores.

Takes plain data — a dict, or df.iloc[i].to_dict() — so that the projection can be tested without a DataFrame, a database or a server.

Why there is a cap. A spaCR object table has 300-600 columns. A MapAnnotation with 400 entries renders in OMERO.web as a 400-row scrolling table in a 300 px panel, which nobody reads and which makes the panel useless for the annotations that were worth showing. So the annotation is a summary, capped at max_pairs (MAX_MAP_PAIRS = 50 by default) and ordered so the useful keys are the ones that survive: priority keys first in the order given, then the row’s own order. When anything is cut, the last pair is TRUNCATION_KEY, saying how many were dropped and that the full table is the CSV attached under NS_FILE. The full table is never only in the annotation.

The result always has at most max_pairs entries, notice included.

Parameters:
  • row – the measurement row, as a mapping.

  • columns – restrict to these columns, in this order (after the priority keys). None uses every key in row.

  • priority – keys promoted to the front when present.

  • max_pairs – the hard cap, including the truncation notice.

  • nan_policy – NAN_KEEP (default — a missing value becomes the string 'NaN', so “measured and missing” is distinguishable from “never computed”) or NAN_DROP (the key is omitted).

  • extra – pairs merged in after row, overriding it — used for provenance such as the spaCR version.

  • float_format – passed to format_map_value().

Returns:

a tuple of (key, value) string pairs.

Raises:

OmeroError – for an unknown nan_policy or max_pairs < 2 (a cap of one leaves no room for both a measurement and a notice).

spacr.omero.missing_omero_message(module: str) → str[source]

Return the full “install the extra” text naming module.

Parameters:

module – the top-level module that could not be imported, e.g. 'omero' or 'Ice'.

Returns:

OMERO_MISSING_MESSAGE formatted for module, with ICE_NOTE appended when module belongs to zeroc-ice.

spacr.omero.omero_indices(well: str) → Tuple[int, int][source]

Invert well_position(): 'A01' -> (0, 0).

The round trip is what makes the export direction possible — matching a spaCR per-well summary back onto the OMERO wells it came from.

Parameters:

well – a well name ('A01', 'aa1', 'AF48') or a spaCR (rowID, columnID) pair already joined, e.g. 'r1_c1' is not accepted — pass the well name.

Returns:

(row, column), both 0-based, as OMERO reports them.

Raises:

OmeroWellError – when well is not a well name.

spacr.omero.parse_object_id(value: Any, expect: str | None = None) → int[source]

Parse value to a positive OMERO id, checking the type it names.

This is the guard that stops a Plate id being handed to the Dataset importer: the mistake is easy (both are integers, both exist on the same server) and the failure without this check is a confusing empty import rather than an error.

Parameters:
  • value – anything parse_object_ref() accepts.

  • expect – the required object type, e.g. 'Dataset'. A reference that names a different type is refused; a bare id, which names no type, is accepted and taken at the caller’s word.

Returns:

the positive integer id.

Raises:

OmeroIdError – for an unusable id, or when the reference names a type other than expect.

spacr.omero.parse_object_ref(value: Any) → OmeroRef[source]

Parse an OMERO object reference into a kind and a positive id.

Accepts, deliberately, every form a user is likely to paste:

  • 123 or '123' — the id alone, kind unknown;

  • 'Dataset:123', 'dataset-123' — id and kind together;

  • 'https://omero.example.org/webclient/?show=plate-42' — what the OMERO.web address bar contains while you are looking at the plate.

Parameters:

value – an int, or a string in one of the forms above, or an OmeroRef (returned unchanged).

Returns:

an OmeroRef.

Raises:

OmeroIdError – for a negative or zero id, a non-numeric id, an empty value, a float, or a type name OMERO does not have. OMERO ids are positive int64 and 0 is never one.

spacr.omero.pixel_size_from(length: Any) → PixelSize[source]

Read a PixelSize out of whatever getPixelSizeX() returned.

omero-py returns different things depending on how it is called and how old the server is: a LengthI object (getValue() / getSymbol() or getUnit()), a plain float (already in µm, by omero-py’s own convention), or None for an uncalibrated image. All three are handled, and only the plain-float case assumes a unit — because in that case omero-py has already made the assumption itself, and this records that it did rather than pretending the unit is unknown.

Parameters:

length – the value returned by getPixelSizeX/Y/Z.

Returns:

a PixelSize; PixelSize(None, None) when there is no calibration.

spacr.omero.plan_annotation(existing: Iterable[Sequence[Any]], namespace: str, mode: str = REPLACE) → AnnotationPlan[source]

Decide whether to overwrite spaCR’s previous annotation or add one.

Pure: existing is a sequence of (annotation_id, namespace) pairs, which is all the decision needs, so the rule is testable without a server.

Under REPLACE — the default — an annotation whose namespace is exactly namespace is updated in place. Nothing is created, nothing is unlinked and nothing is deleted; a re-run therefore leaves one annotation rather than a growing pile, and an annotation in any other namespace (a colleague’s, OMERO’s own bulk annotations) is never even considered. When more than one already exists — only possible after an APPEND run or a concurrent writer — the oldest (lowest id) is updated and the rest are reported in duplicates rather than removed, because deleting an annotation somebody may have linked elsewhere is not a decision an exporter gets to make.

Parameters:
  • existing – (id, namespace) pairs already on the target.

  • namespace – the spaCR namespace being written.

  • mode – REPLACE or APPEND.

Returns:

an AnnotationPlan.

Raises:

OmeroError – for an unknown mode, or a namespace spaCR does not own (this module refuses to write outside SPACR_NAMESPACES, which is the guarantee that makes “replace” safe).

spacr.omero.plan_tag(existing: Iterable[Sequence[Any]], namespace: str, text: str, mode: str = REPLACE) → AnnotationPlan[source]

Decide what to do about a categorical verdict tag.

Tags get their own rule, and the reason is a property of OMERO rather than a preference: a TagAnnotation is a shared object. The tag reading hit on this image is very likely the same row in the database as the tag reading hit on two hundred other images, so editing its text to say not hit would silently relabel all of them. Updating in place is therefore not available here, whatever the mode.

What happens instead:

  • the identical verdict is already linked -> ACTION_UNCHANGED, nothing is written and the export is idempotent;

  • no spaCR verdict is linked -> ACTION_CREATE;

  • a different spaCR verdict is linked -> ACTION_CREATE for the new one, and the old one is reported in duplicates and left in place. Two verdicts on one object is visibly wrong in OMERO.web, which is better than silently rewriting history.

Parameters:
  • existing – (id, namespace, text) triples already on the target.

  • namespace – the spaCR verdict namespace.

  • text – the verdict being written.

  • mode – REPLACE or APPEND; APPEND skips even the idempotence check.

Returns:

an AnnotationPlan.

Raises:

OmeroError – for an unknown mode or a namespace spaCR does not own.

spacr.omero.plane_filename(plate: str, well: str, field_id: int, channel: int, z: int = 1, t: int = 1) → str[source]

Return the Yokogawa filename spaCR expects for one plane.

A thin, deliberate delegation to spacr.convert.target_name(), so there is exactly one definition of the name in spaCR and an OMERO import cannot drift away from a folder conversion.

Parameters:
  • plate – plate token; run it through plate_token() first.

  • well – canonical well id, e.g. 'A01'.

  • field_id – 1-based field (imaging site) id.

  • channel – 1-based channel id.

  • z – 1-based z-slice id.

  • t – 1-based timepoint id.

Returns:

e.g. 'plate1_A01_T0001F001L01A01Z01C01.tif'.

spacr.omero.plate_token(name: Any) → str[source]

Reduce name to a plate token that survives spaCR’s filename regex.

Underscores are stripped along with every other non-alphanumeric character, and the reason is not cosmetic: the cellvoyager regex splits the plate from the well on _, so a plate literally called my run would move the split point and misparse every well. This is the same rule as spacr.convert._sanitise, which is the function that already governs the rest of spaCR’s conversions.

Parameters:

name – any label — an OMERO plate name, a dataset name, an id.

Returns:

a non-empty token of [A-Za-z0-9-], falling back to 'plate' when nothing survives.

spacr.omero.require_omero() → Any[source]

Import and return omero.gateway, or raise a message worth reading.

Returns:

the imported omero.gateway module, which is where BlitzGateway, MapAnnotationWrapper and TagAnnotationWrapper live.

Raises:
  • OmeroExtraMissing – when omero-py (or the Ice runtime beneath it) is not installed. The message names pip install "spacr[omero]".

  • ImportError – unchanged, when the failure is a real bug inside a module that is installed.

spacr.omero.summarise_rows(rows: Sequence[Mapping[str, Any]], *, columns: Sequence[str] | None = None) → Dict[str, Any][source]

Reduce many object rows to one per-well summary, in plain Python.

The mean of each numeric column over the rows that actually have a value there, plus n_objects. Missing values are excluded from their own column’s mean rather than zero-filled: in spaCR a NaN is usually structural (a pathogen_* column is NaN for a cell with no pathogen in it), and zero-filling turns “no pathogen” into “a pathogen of zero size”. A column with no usable value in any row is reported as None, which measurement_pairs() renders as NaN.

Non-numeric columns are carried through when every row agrees on the value — that is how plateID, rowID, columnID and condition reach the well annotation — and dropped when they disagree, because a single well cannot have two plate ids and picking one would be a guess.

Parameters:
  • rows – the per-object rows for one well.

  • columns – restrict to these columns; None uses the union of the rows’ keys.

Returns:

a plain dict, ready for measurement_pairs().

spacr.omero.well_from_image_name(name: Any) → str | None[source]

Recover a well name from an OMERO image name, or return None.

Images in a Dataset have no plate geometry — a Dataset is a flat bag — so the only place a well can come from is the name the acquisition software gave the file. This reads the first delimited token that parses as a well within a 1536-well plate (32 rows, 48 columns).

It is a guess, and it is treated as one: import_dataset() records well_source='name' or 'sequence' per file in the sidecar CSV, and well_from_name=False turns it off entirely.

Parameters:

name – the OMERO image name.

Returns:

a canonical well name ('A01'), or None when the name contains nothing that parses as one.

spacr.omero.well_position(row: Any, column: Any) → WellPosition[source]

Map OMERO’s 0-based (row, column) onto spaCR’s well keys.

This is the single most important line of the import, because everything downstream — every group-by, every plate heatmap, every well-level statistic — is keyed off rowID/columnID, and a plate that comes in one row out is a plate whose controls are in the wrong place.

Row letters are bijective base 26 via spacr.schema.well_id(), so:

well_position(0, 0).well   == 'A01'
well_position(25, 0).well  == 'Z01'
well_position(26, 0).well  == 'AA01'      # not '[01', not IndexError
well_position(31, 47).well == 'AF48'      # a 1536-well plate
Parameters:
  • row – OMERO’s WellWrapper.getRow(), 0-based.

  • column – OMERO’s WellWrapper.getColumn(), 0-based.

Returns:

a WellPosition.

Raises:

OmeroWellError – when either index is missing, not an integer, or negative. OMERO leaves both None on a well that was never placed on the plate, and silently treating that as row 0 would put an unplaced well in A01 alongside a real one.

spacr.omero.well_summary_pairs(rows: Sequence[Mapping[str, Any]], *, columns: Sequence[str] | None = None, **kwargs: Any) → Tuple[Tuple[str, str], ...][source]

Summarise object rows and project them onto key/value pairs.

summarise_rows() followed by measurement_pairs(), which is the combination the per-well export always wants.

Parameters:
  • rows – the per-object rows for one well.

  • columns – restrict the summary to these columns.

  • kwargs – passed to measurement_pairs().

Returns:

a tuple of (key, value) string pairs.

spacr.omero.OMERO_MISSING_MESSAGE = Multiline-String[source]
Show Value
"""Talking to an OMERO server needs the optional `omero` extra, which is not
installed in this environment (missing module: {module}).

Install it with:

    python -m pip install "spacr[omero]"

It is deliberately not part of `spacr[all]`: omero-py depends on zeroc-ice, a
compiled C++ runtime, so it is only installed when you ask for it."""