sal-xarray 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Samuel Jackson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: sal-xarray
3
+ Version: 0.1.0
4
+ Summary: SAL backend for xarray
5
+ Author-email: Samuel Jackson <samuel.jackson@ukaea.uk>
6
+ Maintainer-email: Samuel Jackson <samuel.jackson@ukaea.uk>
7
+ Project-URL: repository, https://github.com/samueljackson92/sal-xarray
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Requires-Python: <3.13,>=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy<2
17
+ Requires-Dist: sal>=1.2.5
18
+ Requires-Dist: xarray>=2025.12.0
19
+ Dynamic: license-file
20
+
21
+ # sal-xarray
22
+
23
+ An xarray backend for SAL (Simple Access Layer) that enables seamless integration of SAL data sources with the xarray ecosystem.
24
+
25
+ ## Overview
26
+
27
+ sal-xarray provides a backend plugin for xarray that allows you to access SAL data sources using xarray's familiar API. It automatically handles the conversion of SAL signals to xarray DataArrays and Datasets, making it easy to work with SAL data in scientific Python workflows.
28
+ ## Features
29
+
30
+ - **xarray Integration**: Access SAL data using xarray's `open_dataset` function
31
+ - **Automatic Conversion**: Converts SAL signals to xarray DataArrays with proper coordinates and metadata
32
+ - **Status Mask Handling**: Includes status mask data alongside signal data
33
+ - **URI-based Access**: Simple URI scheme (`sal://pulse/<shot_number>/<signal_name>`) for accessing data
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install sal-xarray
39
+ ```
40
+
41
+ Or using `uv`:
42
+
43
+ ```bash
44
+ uv pip install sal-xarray
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Open a SAL dataset using xarray:
50
+
51
+ ```python
52
+ import xarray as xr
53
+
54
+ # Open a SAL signal by name and shot number
55
+ ds = xr.open_dataset("sal://pulse/87737/ppf/signal/jetppf/magn/ipla", engine="sal")
56
+
57
+ # Access the data
58
+ data = ds["data"]
59
+ status = ds["status"]
60
+
61
+ # The dataset includes time coordinates
62
+ time = ds["time"]
63
+
64
+ # Access metadata
65
+ units = ds["data"].attrs["units"]
66
+ signal_name = ds["data"].attrs["sal_name"]
67
+ ```
68
+
69
+ The URI format is: `sal://pulse/<shot_number>/<signal_name>`
70
+
71
+ ## Data Structure
72
+
73
+ When you open a SAL dataset, sal-xarray creates an xarray Dataset with:
74
+ - **data**: The signal data as a DataArray
75
+ - **status**: The status data as a DataArray
76
+ - **time**: Time coordinates (dimension)
77
+ - **attrs**: Metadata including units and SAL signal name
78
+
79
+ ## Development
80
+
81
+ ### Setup
82
+
83
+ Clone the repository and install development dependencies:
84
+
85
+ ```bash
86
+ git clone https://github.com/samueljackson92/sal-xarray.git
87
+ cd sal-xarray
88
+ uv sync
89
+ ```
90
+
91
+ ### Running Tests
92
+
93
+ ```bash
94
+ uv run pytest tests/
95
+ ```
96
+
97
+ ### Code Quality
98
+
99
+ The project uses ruff for linting and formatting, and pylint for additional checks:
100
+
101
+ ```bash
102
+ # Run ruff
103
+ uv run ruff check sal_xarray tests
104
+ uv run ruff format sal_xarray tests
105
+
106
+ # Run pylint
107
+ uv run pylint sal_xarray
108
+
109
+ # Run ty
110
+ uv run ty check sal_xarray
111
+ ```
112
+
113
+ ## Contributing
114
+
115
+ Contributions are welcome! Please ensure:
116
+
117
+ 1. Tests pass for any new functionality
118
+ 2. Code follows the project's style guidelines (ruff and pylint)
119
+ 3. Documentation is updated as needed
120
+
121
+ ## License
122
+
123
+ MIT License
@@ -0,0 +1,103 @@
1
+ # sal-xarray
2
+
3
+ An xarray backend for SAL (Simple Access Layer) that enables seamless integration of SAL data sources with the xarray ecosystem.
4
+
5
+ ## Overview
6
+
7
+ sal-xarray provides a backend plugin for xarray that allows you to access SAL data sources using xarray's familiar API. It automatically handles the conversion of SAL signals to xarray DataArrays and Datasets, making it easy to work with SAL data in scientific Python workflows.
8
+ ## Features
9
+
10
+ - **xarray Integration**: Access SAL data using xarray's `open_dataset` function
11
+ - **Automatic Conversion**: Converts SAL signals to xarray DataArrays with proper coordinates and metadata
12
+ - **Status Mask Handling**: Includes status mask data alongside signal data
13
+ - **URI-based Access**: Simple URI scheme (`sal://pulse/<shot_number>/<signal_name>`) for accessing data
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install sal-xarray
19
+ ```
20
+
21
+ Or using `uv`:
22
+
23
+ ```bash
24
+ uv pip install sal-xarray
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Open a SAL dataset using xarray:
30
+
31
+ ```python
32
+ import xarray as xr
33
+
34
+ # Open a SAL signal by name and shot number
35
+ ds = xr.open_dataset("sal://pulse/87737/ppf/signal/jetppf/magn/ipla", engine="sal")
36
+
37
+ # Access the data
38
+ data = ds["data"]
39
+ status = ds["status"]
40
+
41
+ # The dataset includes time coordinates
42
+ time = ds["time"]
43
+
44
+ # Access metadata
45
+ units = ds["data"].attrs["units"]
46
+ signal_name = ds["data"].attrs["sal_name"]
47
+ ```
48
+
49
+ The URI format is: `sal://pulse/<shot_number>/<signal_name>`
50
+
51
+ ## Data Structure
52
+
53
+ When you open a SAL dataset, sal-xarray creates an xarray Dataset with:
54
+ - **data**: The signal data as a DataArray
55
+ - **status**: The status data as a DataArray
56
+ - **time**: Time coordinates (dimension)
57
+ - **attrs**: Metadata including units and SAL signal name
58
+
59
+ ## Development
60
+
61
+ ### Setup
62
+
63
+ Clone the repository and install development dependencies:
64
+
65
+ ```bash
66
+ git clone https://github.com/samueljackson92/sal-xarray.git
67
+ cd sal-xarray
68
+ uv sync
69
+ ```
70
+
71
+ ### Running Tests
72
+
73
+ ```bash
74
+ uv run pytest tests/
75
+ ```
76
+
77
+ ### Code Quality
78
+
79
+ The project uses ruff for linting and formatting, and pylint for additional checks:
80
+
81
+ ```bash
82
+ # Run ruff
83
+ uv run ruff check sal_xarray tests
84
+ uv run ruff format sal_xarray tests
85
+
86
+ # Run pylint
87
+ uv run pylint sal_xarray
88
+
89
+ # Run ty
90
+ uv run ty check sal_xarray
91
+ ```
92
+
93
+ ## Contributing
94
+
95
+ Contributions are welcome! Please ensure:
96
+
97
+ 1. Tests pass for any new functionality
98
+ 2. Code follows the project's style guidelines (ruff and pylint)
99
+ 3. Documentation is updated as needed
100
+
101
+ ## License
102
+
103
+ MIT License
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "sal-xarray"
3
+ version = "0.1.0"
4
+ description = "SAL backend for xarray"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11, <3.13"
7
+ authors = [
8
+ {name = "Samuel Jackson", email = "samuel.jackson@ukaea.uk"},
9
+ ]
10
+ maintainers = [
11
+ {name = "Samuel Jackson", email = "samuel.jackson@ukaea.uk"},
12
+ ]
13
+
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "License :: OSI Approved :: MIT License",
20
+ ]
21
+
22
+ dependencies = [
23
+ "numpy<2",
24
+ "sal>=1.2.5",
25
+ "xarray>=2025.12.0",
26
+ ]
27
+
28
+ [tool.setuptools]
29
+ packages = ["sal_xarray"]
30
+
31
+ [project.urls]
32
+ repository = "https://github.com/samueljackson92/sal-xarray"
33
+
34
+ [tool.ruff]
35
+ exclude = []
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "bump-my-version>=1.2.5",
40
+ "pylint>=4.0.4",
41
+ "pytest>=8.4.1",
42
+ "pytest-mock>=3.15.1",
43
+ "ruff>=0.14.10",
44
+ "ty>=0.0.12",
45
+ ]
46
+
47
+ [project.entry-points."xarray.backends"]
48
+ sal = "sal_xarray.main:SALBackendEntrypoint"
49
+
50
+ [tool.bumpversion]
51
+ current_version = "0.1.0"
52
+ parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
53
+ serialize = ["{major}.{minor}.{patch}"]
54
+ search = "{current_version}"
55
+ replace = "{new_version}"
56
+ regex = false
57
+ ignore_missing_version = false
58
+ ignore_missing_files = false
59
+ tag = true
60
+ sign_tags = false
61
+ tag_name = "v{new_version}"
62
+ tag_message = "Bump version: {current_version} → {new_version}"
63
+ allow_dirty = false
64
+ commit = true
65
+ message = "Bump version: {current_version} → {new_version}"
66
+ commit_args = ""
67
+
68
+ [[tool.bumpversion.files]]
69
+ filename = "pyproject.toml"
70
+ search = 'version = "{current_version}"'
71
+ replace = 'version = "{new_version}"'
@@ -0,0 +1,116 @@
1
+ """Xarray SAL backend entrypoint."""
2
+
3
+ import os
4
+ import re
5
+ from typing import Any, Iterable
6
+
7
+ import numpy as np
8
+ import xarray as xr
9
+ from sal.client import SALClient
10
+
11
+ from xarray.backends import BackendEntrypoint
12
+ from xarray.backends.common import ReadBuffer
13
+ from xarray.backends.common import AbstractDataStore
14
+
15
+
16
+ class SALBackendEntrypoint(BackendEntrypoint):
17
+ """Xarray SAL backend entrypoint."""
18
+
19
+ def open_dataset(
20
+ self,
21
+ filename_or_obj: str
22
+ | os.PathLike[Any]
23
+ | ReadBuffer
24
+ | bytes
25
+ | memoryview
26
+ | AbstractDataStore,
27
+ *,
28
+ drop_variables: str | Iterable[str] | None = None,
29
+ host: str = "https://sal.jetdata.eu",
30
+ ) -> xr.Dataset:
31
+ """Open a SAL dataset given a signal name and shot number.
32
+
33
+ Parameters
34
+ ----------
35
+ filename_or_obj : str
36
+ SAL dataset specified as sal://pulse/<shot_number>/<signal_name>
37
+ drop_variables : str or list of str, optional
38
+ Variables to drop from the dataset (not used).
39
+ """
40
+
41
+ if not isinstance(filename_or_obj, str):
42
+ raise ValueError("SAL backend only supports string filenames.")
43
+
44
+ if "sal://" not in filename_or_obj:
45
+ raise ValueError("SAL dataset must start with the sal:// scheme")
46
+
47
+ name = filename_or_obj.replace("sal://", "")
48
+ match = re.search(r"sal://pulse/(\d+)/", filename_or_obj)
49
+
50
+ if not match:
51
+ raise ValueError(
52
+ "Invalid SAL URL format. Expected sal://pulse/<shot_number>/<signal_name>"
53
+ )
54
+
55
+ shot = int(match.group(1))
56
+ name = "/" + name
57
+
58
+ client = SALClient(host=host)
59
+ try:
60
+ signal = client.get(name)
61
+ except Exception as e:
62
+ raise RuntimeError(f"Failed to retrieve SAL signal '{name}': {e}") from e
63
+
64
+ data = self.parse_data(signal)
65
+ data.attrs["shot_id"] = shot
66
+
67
+ status = xr.DataArray(signal.mask.status, dims=data.dims, coords=data.coords)
68
+ status.attrs["description"] = signal.mask.description
69
+ status.attrs["flag_values"] = np.unique(signal.mask.status)
70
+ status.attrs["flag_meanings"] = signal.mask.key.tolist()
71
+
72
+ dataset = data.to_dataset(name="data")
73
+ dataset["status"] = status
74
+ return dataset
75
+
76
+ def parse_data(self, signal) -> xr.DataArray:
77
+ """Parse a SAL LeafReport into an xarray DataArray."""
78
+
79
+ dims = {}
80
+ for index, item in enumerate(signal.dimensions):
81
+ name = f"dim_{index}"
82
+ if item.temporal:
83
+ name = "time"
84
+ dim = xr.DataArray(item.data, dims=[name], name=name)
85
+ dim.attrs["description"] = item.description
86
+ dim.attrs["units"] = item.units
87
+
88
+ dims[dim.name] = dim
89
+
90
+ data = xr.DataArray(
91
+ signal.data,
92
+ dims=[d.name for d in dims.values()],
93
+ coords={d.name: d for d in dims.values()},
94
+ )
95
+
96
+ data.attrs["description"] = signal.description
97
+ data.attrs["units"] = signal.units
98
+ data.attrs["sal_name"] = name
99
+
100
+ return data
101
+
102
+ def open_datatree(self, filename_or_obj, *, drop_variables=None):
103
+ self.open_dataset(filename_or_obj, drop_variables=drop_variables)
104
+
105
+ def open_groups_as_dict(self, filename_or_obj, **kwargs):
106
+ """Open groups as a dictionary (not supported for SAL backend)."""
107
+ raise NotImplementedError("SAL backend does not support opening groups")
108
+
109
+ open_dataset_parameters = ["filename_or_obj", "drop_variables"]
110
+
111
+ def guess_can_open(self, filename_or_obj):
112
+ return filename_or_obj.startswith("sal://")
113
+
114
+ description = "Use SAL data in Xarray"
115
+
116
+ url = "https://github.com/samueljackson92/sal-xarray"
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: sal-xarray
3
+ Version: 0.1.0
4
+ Summary: SAL backend for xarray
5
+ Author-email: Samuel Jackson <samuel.jackson@ukaea.uk>
6
+ Maintainer-email: Samuel Jackson <samuel.jackson@ukaea.uk>
7
+ Project-URL: repository, https://github.com/samueljackson92/sal-xarray
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Requires-Python: <3.13,>=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy<2
17
+ Requires-Dist: sal>=1.2.5
18
+ Requires-Dist: xarray>=2025.12.0
19
+ Dynamic: license-file
20
+
21
+ # sal-xarray
22
+
23
+ An xarray backend for SAL (Simple Access Layer) that enables seamless integration of SAL data sources with the xarray ecosystem.
24
+
25
+ ## Overview
26
+
27
+ sal-xarray provides a backend plugin for xarray that allows you to access SAL data sources using xarray's familiar API. It automatically handles the conversion of SAL signals to xarray DataArrays and Datasets, making it easy to work with SAL data in scientific Python workflows.
28
+ ## Features
29
+
30
+ - **xarray Integration**: Access SAL data using xarray's `open_dataset` function
31
+ - **Automatic Conversion**: Converts SAL signals to xarray DataArrays with proper coordinates and metadata
32
+ - **Status Mask Handling**: Includes status mask data alongside signal data
33
+ - **URI-based Access**: Simple URI scheme (`sal://pulse/<shot_number>/<signal_name>`) for accessing data
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install sal-xarray
39
+ ```
40
+
41
+ Or using `uv`:
42
+
43
+ ```bash
44
+ uv pip install sal-xarray
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Open a SAL dataset using xarray:
50
+
51
+ ```python
52
+ import xarray as xr
53
+
54
+ # Open a SAL signal by name and shot number
55
+ ds = xr.open_dataset("sal://pulse/87737/ppf/signal/jetppf/magn/ipla", engine="sal")
56
+
57
+ # Access the data
58
+ data = ds["data"]
59
+ status = ds["status"]
60
+
61
+ # The dataset includes time coordinates
62
+ time = ds["time"]
63
+
64
+ # Access metadata
65
+ units = ds["data"].attrs["units"]
66
+ signal_name = ds["data"].attrs["sal_name"]
67
+ ```
68
+
69
+ The URI format is: `sal://pulse/<shot_number>/<signal_name>`
70
+
71
+ ## Data Structure
72
+
73
+ When you open a SAL dataset, sal-xarray creates an xarray Dataset with:
74
+ - **data**: The signal data as a DataArray
75
+ - **status**: The status data as a DataArray
76
+ - **time**: Time coordinates (dimension)
77
+ - **attrs**: Metadata including units and SAL signal name
78
+
79
+ ## Development
80
+
81
+ ### Setup
82
+
83
+ Clone the repository and install development dependencies:
84
+
85
+ ```bash
86
+ git clone https://github.com/samueljackson92/sal-xarray.git
87
+ cd sal-xarray
88
+ uv sync
89
+ ```
90
+
91
+ ### Running Tests
92
+
93
+ ```bash
94
+ uv run pytest tests/
95
+ ```
96
+
97
+ ### Code Quality
98
+
99
+ The project uses ruff for linting and formatting, and pylint for additional checks:
100
+
101
+ ```bash
102
+ # Run ruff
103
+ uv run ruff check sal_xarray tests
104
+ uv run ruff format sal_xarray tests
105
+
106
+ # Run pylint
107
+ uv run pylint sal_xarray
108
+
109
+ # Run ty
110
+ uv run ty check sal_xarray
111
+ ```
112
+
113
+ ## Contributing
114
+
115
+ Contributions are welcome! Please ensure:
116
+
117
+ 1. Tests pass for any new functionality
118
+ 2. Code follows the project's style guidelines (ruff and pylint)
119
+ 3. Documentation is updated as needed
120
+
121
+ ## License
122
+
123
+ MIT License
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ sal_xarray/main.py
5
+ sal_xarray.egg-info/PKG-INFO
6
+ sal_xarray.egg-info/SOURCES.txt
7
+ sal_xarray.egg-info/dependency_links.txt
8
+ sal_xarray.egg-info/entry_points.txt
9
+ sal_xarray.egg-info/requires.txt
10
+ sal_xarray.egg-info/top_level.txt
11
+ tests/test_backend.py
@@ -0,0 +1,2 @@
1
+ [xarray.backends]
2
+ sal = sal_xarray.main:SALBackendEntrypoint
@@ -0,0 +1,3 @@
1
+ numpy<2
2
+ sal>=1.2.5
3
+ xarray>=2025.12.0
@@ -0,0 +1 @@
1
+ sal_xarray
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,196 @@
1
+ """Tests for SAL xarray backend."""
2
+
3
+ from unittest.mock import Mock, patch
4
+
5
+ import numpy as np
6
+ import pytest
7
+ import xarray as xr
8
+
9
+ from sal_xarray.main import SALBackendEntrypoint
10
+
11
+
12
+ @pytest.fixture
13
+ def mock_signal():
14
+ """Create a mock SAL signal object."""
15
+ signal = Mock()
16
+
17
+ # Mock dimension
18
+ dim = Mock()
19
+ dim.data = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
20
+ dim.temporal = True
21
+ dim.description = "Time dimension"
22
+ dim.units = "s"
23
+
24
+ signal.dimensions = [dim]
25
+ signal.data = np.array([10.0, 20.0, 30.0, 40.0, 50.0])
26
+ signal.description = "Test signal"
27
+ signal.units = "V"
28
+
29
+ # Mock mask
30
+ mask = Mock()
31
+ mask.status = np.array([0, 0, 1, 0, 0])
32
+ mask.description = "Data quality mask"
33
+ mask.key = np.array(["good", "bad"])
34
+ signal.mask = mask
35
+
36
+ return signal
37
+
38
+
39
+ @pytest.fixture
40
+ def mock_sal_client(mock_signal):
41
+ """Create a mock SAL client."""
42
+ with patch("sal_xarray.main.SALClient") as MockClient:
43
+ client_instance = Mock()
44
+ client_instance.get.return_value = mock_signal
45
+ MockClient.return_value = client_instance
46
+ yield MockClient
47
+
48
+
49
+ class TestSALBackendEntrypoint:
50
+ """Tests for SALBackendEntrypoint."""
51
+
52
+ def test_guess_can_open_valid(self):
53
+ """Test that guess_can_open returns True for sal:// URLs."""
54
+ backend = SALBackendEntrypoint()
55
+ assert backend.guess_can_open("sal://pulse/12345/signal_name")
56
+
57
+ def test_guess_can_open_invalid(self):
58
+ """Test that guess_can_open returns False for non-sal URLs."""
59
+ backend = SALBackendEntrypoint()
60
+ assert not backend.guess_can_open("http://example.com")
61
+ assert not backend.guess_can_open("file://data.nc")
62
+
63
+ def test_open_dataset_invalid_type(self):
64
+ """Test that open_dataset raises error for non-string input."""
65
+ backend = SALBackendEntrypoint()
66
+ with pytest.raises(
67
+ ValueError, match="SAL backend only supports string filenames"
68
+ ):
69
+ backend.open_dataset(123)
70
+
71
+ def test_open_dataset_missing_scheme(self):
72
+ """Test that open_dataset raises error when sal:// scheme is missing."""
73
+ backend = SALBackendEntrypoint()
74
+ with pytest.raises(
75
+ ValueError, match="SAL dataset must start with the sal:// scheme"
76
+ ):
77
+ backend.open_dataset("pulse/12345/signal_name")
78
+
79
+ def test_open_dataset_invalid_format(self):
80
+ """Test that open_dataset raises error for invalid URL format."""
81
+ backend = SALBackendEntrypoint()
82
+ with pytest.raises(ValueError, match="Invalid SAL URL format"):
83
+ backend.open_dataset("sal://invalid/format")
84
+
85
+ def test_open_dataset_success(self, mock_sal_client, mock_signal):
86
+ """Test successful dataset opening with mocked SAL client."""
87
+ backend = SALBackendEntrypoint()
88
+ ds = backend.open_dataset("sal://pulse/12345/signal_name")
89
+
90
+ # Check that dataset was created
91
+ assert isinstance(ds, xr.Dataset)
92
+
93
+ # Check that data variable exists
94
+ assert "data" in ds.data_vars
95
+ assert "status" in ds.data_vars
96
+
97
+ # Check dimensions
98
+ assert "time" in ds.dims
99
+ assert ds.sizes["time"] == 5
100
+
101
+ # Check data values
102
+ np.testing.assert_array_equal(ds["data"].values, mock_signal.data)
103
+
104
+ # Check attributes
105
+ assert ds["data"].attrs["description"] == "Test signal"
106
+ assert ds["data"].attrs["units"] == "V"
107
+ assert ds["data"].attrs["shot_id"] == 12345
108
+
109
+ # Check status variable
110
+ np.testing.assert_array_equal(ds["status"].values, mock_signal.mask.status)
111
+ assert ds["status"].attrs["description"] == "Data quality mask"
112
+
113
+ # Check that SALClient was called correctly
114
+ mock_sal_client.assert_called_once_with(host="https://sal.jetdata.eu")
115
+ mock_sal_client.return_value.get.assert_called_once()
116
+
117
+ def test_open_dataset_custom_host(self, mock_sal_client):
118
+ """Test opening dataset with custom host."""
119
+ backend = SALBackendEntrypoint()
120
+ custom_host = "https://custom.sal.server"
121
+ backend.open_dataset("sal://pulse/12345/signal_name", host=custom_host)
122
+
123
+ mock_sal_client.assert_called_once_with(host=custom_host)
124
+
125
+ def test_open_dataset_with_multiple_dimensions(self, mock_sal_client):
126
+ """Test opening dataset with multiple dimensions."""
127
+ # Create mock signal with two dimensions
128
+ signal = Mock()
129
+
130
+ dim0 = Mock()
131
+ dim0.data = np.array([0.0, 1.0, 2.0])
132
+ dim0.temporal = False
133
+ dim0.description = "Spatial dimension"
134
+ dim0.units = "m"
135
+
136
+ dim1 = Mock()
137
+ dim1.data = np.array([0.0, 1.0, 2.0, 3.0])
138
+ dim1.temporal = True
139
+ dim1.description = "Time dimension"
140
+ dim1.units = "s"
141
+
142
+ signal.dimensions = [dim0, dim1]
143
+ signal.data = np.random.rand(3, 4)
144
+ signal.description = "2D signal"
145
+ signal.units = "A"
146
+
147
+ mask = Mock()
148
+ mask.status = np.zeros((3, 4), dtype=int)
149
+ mask.description = "Mask"
150
+ mask.key = np.array(["good", "bad"])
151
+ signal.mask = mask
152
+
153
+ mock_sal_client.return_value.get.return_value = signal
154
+
155
+ backend = SALBackendEntrypoint()
156
+ ds = backend.open_dataset("sal://pulse/12345/signal_name")
157
+
158
+ # Check dimensions
159
+ assert "dim_0" in ds.dims
160
+ assert "time" in ds.dims
161
+ assert ds.sizes["dim_0"] == 3
162
+ assert ds.sizes["time"] == 4
163
+
164
+ # Check data shape
165
+ assert ds["data"].shape == (3, 4)
166
+
167
+ def test_open_dataset_client_error(self, mock_sal_client):
168
+ """Test that open_dataset handles SAL client errors."""
169
+ mock_sal_client.return_value.get.side_effect = Exception("Connection failed")
170
+
171
+ backend = SALBackendEntrypoint()
172
+ with pytest.raises(RuntimeError, match="Failed to retrieve SAL signal"):
173
+ backend.open_dataset("sal://pulse/12345/signal_name")
174
+
175
+ def test_open_dataset_different_shot_numbers(self, mock_sal_client):
176
+ """Test opening datasets with different shot numbers."""
177
+ backend = SALBackendEntrypoint()
178
+
179
+ # Test with different shot numbers
180
+ for shot_num in [1, 100, 99999]:
181
+ ds = backend.open_dataset(f"sal://pulse/{shot_num}/signal_name")
182
+ assert ds["data"].attrs["shot_id"] == shot_num
183
+
184
+
185
+ class TestXarrayIntegration:
186
+ """Test integration with xarray's open_dataset function."""
187
+
188
+ def test_xr_open_dataset_with_engine(self, mock_sal_client):
189
+ """Test using xr.open_dataset with engine='sal'."""
190
+ # This test requires the backend to be registered
191
+ # In practice, this happens via entry_points in pyproject.toml
192
+ ds = xr.open_dataset("sal://pulse/12345/signal_name", engine="sal")
193
+
194
+ assert isinstance(ds, xr.Dataset)
195
+ assert "data" in ds.data_vars
196
+ assert "status" in ds.data_vars