|
42 | 42 | T = TypeVar("T", bound="BaseGraph") |
43 | 43 |
|
44 | 44 |
|
| 45 | +class MetadataView(dict[str, Any]): |
| 46 | + """Dictionary-like metadata view that syncs mutations back to the graph.""" |
| 47 | + |
| 48 | + _MISSING = object() |
| 49 | + |
| 50 | + def __init__( |
| 51 | + self, |
| 52 | + graph: "BaseGraph", |
| 53 | + data: dict[str, Any], |
| 54 | + *, |
| 55 | + is_public: bool = True, |
| 56 | + ) -> None: |
| 57 | + super().__init__(data) |
| 58 | + self._graph = graph |
| 59 | + self._is_public = is_public |
| 60 | + |
| 61 | + def __setitem__(self, key: str, value: Any) -> None: |
| 62 | + self._graph._set_metadata_with_validation(is_public=self._is_public, **{key: value}) |
| 63 | + super().__setitem__(key, value) |
| 64 | + |
| 65 | + def __delitem__(self, key: str) -> None: |
| 66 | + self._graph._remove_metadata_with_validation(key, is_public=self._is_public) |
| 67 | + super().__delitem__(key) |
| 68 | + |
| 69 | + def pop(self, key: str, default: Any = _MISSING) -> Any: |
| 70 | + self._graph._validate_metadata_key(key, is_public=self._is_public) |
| 71 | + |
| 72 | + if key not in self: |
| 73 | + if default is self._MISSING: |
| 74 | + raise KeyError(key) |
| 75 | + return default |
| 76 | + |
| 77 | + value = super().__getitem__(key) |
| 78 | + self._graph._remove_metadata_with_validation(key, is_public=self._is_public) |
| 79 | + super().pop(key, None) |
| 80 | + return value |
| 81 | + |
| 82 | + def popitem(self) -> tuple[str, Any]: |
| 83 | + key, value = super().popitem() |
| 84 | + self._graph._remove_metadata_with_validation(key, is_public=self._is_public) |
| 85 | + return key, value |
| 86 | + |
| 87 | + def clear(self) -> None: |
| 88 | + keys = list(self.keys()) |
| 89 | + for key in keys: |
| 90 | + self._graph._remove_metadata_with_validation(key, is_public=self._is_public) |
| 91 | + super().clear() |
| 92 | + |
| 93 | + def setdefault(self, key: str, default: Any = None) -> Any: |
| 94 | + if key in self: |
| 95 | + return super().__getitem__(key) |
| 96 | + self._graph._set_metadata_with_validation(is_public=self._is_public, **{key: default}) |
| 97 | + super().__setitem__(key, default) |
| 98 | + return default |
| 99 | + |
| 100 | + def update(self, *args, **kwargs) -> None: |
| 101 | + updates = dict(*args, **kwargs) |
| 102 | + if updates: |
| 103 | + self._graph._set_metadata_with_validation(is_public=self._is_public, **updates) |
| 104 | + super().update(updates) |
| 105 | + |
| 106 | + |
45 | 107 | class BaseGraph(abc.ABC): |
46 | 108 | """ |
47 | 109 | Base class for a graph backend. |
48 | 110 | """ |
49 | 111 |
|
| 112 | + _PRIVATE_METADATA_PREFIX = "__private_" |
| 113 | + |
50 | 114 | node_added = Signal(int, object) |
51 | 115 | node_removed = Signal(int, object) |
52 | 116 | node_updated = Signal(int, object, object) |
@@ -1187,7 +1251,8 @@ def from_other(cls: type[T], other: "BaseGraph", **kwargs) -> T: |
1187 | 1251 | node_attrs = node_attrs.drop(DEFAULT_ATTR_KEYS.NODE_ID) |
1188 | 1252 |
|
1189 | 1253 | graph = cls(**kwargs) |
1190 | | - graph.update_metadata(**other.metadata()) |
| 1254 | + graph.metadata.update(other.metadata) |
| 1255 | + graph._private_metadata.update(other._private_metadata_for_copy()) |
1191 | 1256 |
|
1192 | 1257 | current_node_attr_schemas = graph._node_attr_schemas() |
1193 | 1258 | for k, v in other._node_attr_schemas().items(): |
@@ -1792,7 +1857,8 @@ def to_geff( |
1792 | 1857 | for k, v in edge_attrs.to_dict().items() |
1793 | 1858 | } |
1794 | 1859 |
|
1795 | | - td_metadata = self.metadata().copy() |
| 1860 | + td_metadata = self.metadata.copy() |
| 1861 | + td_metadata.update(self._private_metadata_for_copy()) |
1796 | 1862 | td_metadata.pop("geff", None) # avoid geff being written multiple times |
1797 | 1863 |
|
1798 | 1864 | geff_metadata = geff.GeffMetadata( |
@@ -1830,57 +1896,88 @@ def to_geff( |
1830 | 1896 | zarr_format=zarr_format, |
1831 | 1897 | ) |
1832 | 1898 |
|
1833 | | - @abc.abstractmethod |
1834 | | - def metadata(self) -> dict[str, Any]: |
| 1899 | + @property |
| 1900 | + def metadata(self) -> MetadataView: |
1835 | 1901 | """ |
1836 | 1902 | Return the metadata of the graph. |
1837 | 1903 |
|
1838 | 1904 | Returns |
1839 | 1905 | ------- |
1840 | | - dict[str, Any] |
| 1906 | + MetadataView |
1841 | 1907 | The metadata of the graph as a dictionary. |
1842 | 1908 |
|
1843 | 1909 | Examples |
1844 | 1910 | -------- |
1845 | 1911 | ```python |
1846 | | - metadata = graph.metadata() |
| 1912 | + metadata = graph.metadata |
1847 | 1913 | print(metadata["shape"]) |
1848 | 1914 | ``` |
1849 | 1915 | """ |
| 1916 | + return MetadataView( |
| 1917 | + graph=self, |
| 1918 | + data={k: v for k, v in self._metadata().items() if not self._is_private_metadata_key(k)}, |
| 1919 | + is_public=True, |
| 1920 | + ) |
1850 | 1921 |
|
1851 | | - @abc.abstractmethod |
1852 | | - def update_metadata(self, **kwargs) -> None: |
| 1922 | + @property |
| 1923 | + def _private_metadata(self) -> MetadataView: |
| 1924 | + return MetadataView( |
| 1925 | + graph=self, |
| 1926 | + data={k: v for k, v in self._metadata().items() if self._is_private_metadata_key(k)}, |
| 1927 | + is_public=False, |
| 1928 | + ) |
| 1929 | + |
| 1930 | + def _private_metadata_for_copy(self) -> dict[str, Any]: |
1853 | 1931 | """ |
1854 | | - Set or update metadata for the graph. |
| 1932 | + Return private metadata entries that should be propagated by `from_other` or `to_geff`. |
| 1933 | + Backends can override this to exclude backend-specific private metadata. |
| 1934 | + """ |
| 1935 | + return dict(self._private_metadata) |
1855 | 1936 |
|
1856 | | - Parameters |
1857 | | - ---------- |
1858 | | - **kwargs : Any |
1859 | | - The metadata items to set by key. Values will be stored as JSON. |
| 1937 | + @classmethod |
| 1938 | + def _is_private_metadata_key(cls, key: str) -> bool: |
| 1939 | + return key.startswith(cls._PRIVATE_METADATA_PREFIX) |
| 1940 | + |
| 1941 | + def _validate_metadata_key(self, key: str, *, is_public: bool) -> None: |
| 1942 | + if not isinstance(key, str): |
| 1943 | + raise TypeError(f"Metadata key must be a string. Got {type(key)}.") |
| 1944 | + is_private_key = self._is_private_metadata_key(key) |
| 1945 | + if is_public and is_private_key: |
| 1946 | + raise ValueError(f"Metadata key '{key}' is reserved for internal use.") |
| 1947 | + if not is_public and not is_private_key: |
| 1948 | + raise ValueError( |
| 1949 | + f"Metadata key '{key}' is not private. Private metadata keys must start with " |
| 1950 | + f"'{self._PRIVATE_METADATA_PREFIX}'." |
| 1951 | + ) |
1860 | 1952 |
|
1861 | | - Examples |
1862 | | - -------- |
1863 | | - ```python |
1864 | | - graph.update_metadata(shape=[1, 25, 25], path="path/to/image.ome.zarr") |
1865 | | - graph.update_metadata(description="Tracking data from experiment 1") |
1866 | | - ``` |
1867 | | - """ |
| 1953 | + def _validate_metadata_keys(self, keys: Sequence[str], *, is_public: bool) -> None: |
| 1954 | + for key in keys: |
| 1955 | + self._validate_metadata_key(key, is_public=is_public) |
| 1956 | + |
| 1957 | + def _set_metadata_with_validation(self, is_public: bool = True, **kwargs) -> None: |
| 1958 | + self._validate_metadata_keys(kwargs.keys(), is_public=is_public) |
| 1959 | + self._update_metadata(**kwargs) |
| 1960 | + |
| 1961 | + def _remove_metadata_with_validation(self, key: str, *, is_public: bool = True) -> None: |
| 1962 | + self._validate_metadata_key(key, is_public=is_public) |
| 1963 | + self._remove_metadata(key) |
1868 | 1964 |
|
1869 | 1965 | @abc.abstractmethod |
1870 | | - def remove_metadata(self, key: str) -> None: |
| 1966 | + def _metadata(self) -> dict[str, Any]: |
| 1967 | + """ |
| 1968 | + Return the full metadata including private keys. |
1871 | 1969 | """ |
1872 | | - Remove a metadata key from the graph. |
1873 | 1970 |
|
1874 | | - Parameters |
1875 | | - ---------- |
1876 | | - key : str |
1877 | | - The key of the metadata to remove. |
| 1971 | + @abc.abstractmethod |
| 1972 | + def _update_metadata(self, **kwargs) -> None: |
| 1973 | + """ |
| 1974 | + Backend-specific metadata update implementation without public key validation. |
| 1975 | + """ |
1878 | 1976 |
|
1879 | | - Examples |
1880 | | - -------- |
1881 | | - ```python |
1882 | | - graph.remove_metadata("shape") |
1883 | | - ``` |
| 1977 | + @abc.abstractmethod |
| 1978 | + def _remove_metadata(self, key: str) -> None: |
| 1979 | + """ |
| 1980 | + Backend-specific metadata removal implementation without public key validation. |
1884 | 1981 | """ |
1885 | 1982 |
|
1886 | 1983 | def to_traccuracy_graph(self, array_view_kwargs: dict[str, Any] | None = None) -> "TrackingGraph": |
|
0 commit comments