Repository: aeye-lab/pymovements
Pull request #1032: depr: deprecate `utils.filters`
Author: login: dkrako
Labels: name: deprecation
Created: 2025-03-13T16:05:20Z | Merged: 2025-03-25T18:36:54Z

## :warning: Deprecation

`utils.filters` is deprecated and will be removed in pymovements v0.26.0

## Description

Deprecate `utils.filters` as none of the included are intended for the end-user.

The following warnings are printed:

```
DeprecationWarning: Call to deprecated function (or staticmethod) filter_candidates_remove_nans. (This function will be removed in v0.26.0.) -- Deprecated since version v0.21.0.
```

```
DeprecationWarning: Call to deprecated function (or staticmethod) events_split_nans. (This function will be removed in v0.26.0.) -- Deprecated since version v0.21.0.
```


partly resolves #999

## Implemented changes

- [x] moved `utils/filters.py` to `events/_utils/_paths.py` and adjusted to new location
- [x] use `events._utils.filters` in remaining source
- [x] created deprecated function in `utils/filters.py`
- [x] moved `tests/unit/utils/filters_test.py` to `tests/unit/events/_utils/_filters_test.py`
- [x] add test that functions are correctly deprecated
- [x] add test that functions are removed in planned version


Linked issue #999: deprecate utils modules that are not intended for end-users
## Description of the problem
Most of our utils modules are not really intended to be used by end-users , but should be used via high-level interfaces that call these methods.

This follows an anti-pattern where there is high coupling between the `utils` module and other modules and reduced cohesion inside modules like `dataset`.

Also this crowds our online documentation with irrelevant functions no end-user will need.

## Description of a solution

Instead, we should increase the cohesion inside each module and reduce coupling between modules.

Candidates are:
- [x] `utils.aois` #1080 
- [x] `utils.archives` -> `dataset._utils.archives` #1028 
- [x] `utils.checks` -> `_utils._checks` #1094 
- [x] remove `utils.decorators` #1008 
- [x] `utils.downloads` -> `dataset._utils.downloads` #1030 
- [x] `utils.filters` -> `events._utils._filters` #1032
- [x] `utils.parsing` -> `gaze._utils.parsing` #976 
- [x] `utils.paths` -> `_utils._paths` #1029 
- [ ] `utils.plotting` -> `plotting._utils.matlab` #1081 
- [x] `utils.strings` -> `_utils._strings` #1031 


Diff:
diff --git a/src/pymovements/events/_utils/__init__.py b/src/pymovements/events/_utils/__init__.py
new file mode 100644
index 000000000..6786db225
--- /dev/null
+++ b/src/pymovements/events/_utils/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
diff --git a/src/pymovements/events/_utils/_filters.py b/src/pymovements/events/_utils/_filters.py
new file mode 100644
index 000000000..cf2f14759
--- /dev/null
+++ b/src/pymovements/events/_utils/_filters.py
@@ -0,0 +1,99 @@
+# Copyright (c) 2023-2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Provides filter specific funtions."""
+from __future__ import annotations
+
+import numpy as np
+
+from pymovements.gaze.transforms_numpy import consecutive
+
+
+def filter_candidates_remove_nans(
+        candidates: list[np.ndarray],
+        values: np.ndarray,
+) -> list[np.ndarray]:
+    """Filter a list of candidates for an event-detection algorithm.
+
+    Removes leading and ending np.nans for all candidates in candidates
+
+    Parameters
+    ----------
+    candidates: list[np.ndarray]
+        List of candidates; each candidate consists of a list of indices
+    values: np.ndarray
+        shape (N, 1) or shape (N, 2)
+        Corresponding continuous 1D/2D values time series.
+
+    Returns
+    -------
+    list[np.ndarray]
+        Returns a filtered list of candidates.
+    """
+    values = np.array(values)
+    return_candidates = []
+    for candidate in candidates:
+        if len(candidate) == 0:
+            continue
+        cand_values = values[np.array(candidate)]
+        start_id = 0
+        while np.sum(np.isnan(cand_values[start_id, :])) > 0:
+            start_id += 1
+        end_id = len(cand_values) - 1
+        while np.sum(np.isnan(cand_values[end_id, :])) > 0:
+            end_id -= 1
+        cur_candidate = list(candidate[start_id:end_id + 1])
+        return_candidates.append(np.array(cur_candidate))
+    return return_candidates
+
+
+def events_split_nans(
+        candidates: list[np.ndarray],
+        values: np.ndarray,
+) -> list[np.ndarray]:
+    """Filter a list of candidates for an event-detection algorithm.
+
+    Splits events if np.nans are within an event
+
+    Parameters
+    ----------
+    candidates: list[np.ndarray]
+        List of candidates; each candidate consists of a list of indices
+    values: np.ndarray
+        shape (N, 1) or shape (N, 2)
+        Corresponding continuous 1D/2D values time series.
+
+    Returns
+    -------
+    list[np.ndarray]
+        Returns a filtered list of candidates.
+    """
+    values = np.array(values)
+    return_candidates = []
+    for candidate in candidates:
+        if len(candidate) == 0:
+            continue
+        cur_values = values[np.array(candidate)]
+        nan_candidates = consecutive(arr=np.where(~np.isnan(np.sum(cur_values, axis=1)))[0])
+        cand_list = [
+            np.array(candidate[candidate_indices[0]:candidate_indices[-1] + 1])
+            for candidate_indices in nan_candidates
+        ]
+        return_candidates += cand_list
+    return return_candidates
diff --git a/src/pymovements/events/detection/_idt.py b/src/pymovements/events/detection/_idt.py
index 82916aa04..a1dfe8396 100644
--- a/src/pymovements/events/detection/_idt.py
+++ b/src/pymovements/events/detection/_idt.py
@@ -22,11 +22,11 @@
 
 import numpy as np
 
+from pymovements.events._utils._filters import events_split_nans
+from pymovements.events._utils._filters import filter_candidates_remove_nans
 from pymovements.events.detection._library import register_event_detection
 from pymovements.events.frame import EventDataFrame
 from pymovements.utils import checks
-from pymovements.utils.filters import events_split_nans
-from pymovements.utils.filters import filter_candidates_remove_nans
 
 
 def dispersion(positions: list[list[float]] | np.ndarray) -> float:
diff --git a/src/pymovements/events/detection/_ivt.py b/src/pymovements/events/detection/_ivt.py
index 1a82ff953..030720b99 100644
--- a/src/pymovements/events/detection/_ivt.py
+++ b/src/pymovements/events/detection/_ivt.py
@@ -22,12 +22,12 @@
 
 import numpy as np
 
+from pymovements.events._utils._filters import filter_candidates_remove_nans
 from pymovements.events.detection._library import register_event_detection
 from pymovements.events.frame import EventDataFrame
 from pymovements.gaze.transforms_numpy import consecutive
 from pymovements.gaze.transforms_numpy import norm
 from pymovements.utils import checks
-from pymovements.utils.filters import filter_candidates_remove_nans
 
 
 @register_event_detection
diff --git a/src/pymovements/events/detection/_microsaccades.py b/src/pymovements/events/detection/_microsaccades.py
index 5cb5a2ce8..c192cb1f1 100644
--- a/src/pymovements/events/detection/_microsaccades.py
+++ b/src/pymovements/events/detection/_microsaccades.py
@@ -24,11 +24,11 @@
 
 import numpy as np
 
+from pymovements.events._utils._filters import filter_candidates_remove_nans
 from pymovements.events.detection._library import register_event_detection
 from pymovements.events.frame import EventDataFrame
 from pymovements.gaze.transforms_numpy import consecutive
 from pymovements.utils import checks
-from pymovements.utils.filters import filter_candidates_remove_nans
 
 
 @register_event_detection
diff --git a/src/pymovements/utils/filters.py b/src/pymovements/utils/filters.py
index cf2f14759..7fcd13297 100644
--- a/src/pymovements/utils/filters.py
+++ b/src/pymovements/utils/filters.py
@@ -17,14 +17,25 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-"""Provides filter specific funtions."""
+"""Provides filter specific funtions.
+
+.. deprecated:: v0.21.1
+   This module will be removed in v0.26.0.
+"""
 from __future__ import annotations
 
 import numpy as np
+from deprecated.sphinx import deprecated
 
-from pymovements.gaze.transforms_numpy import consecutive
+from pymovements.events._utils._filters import events_split_nans as _events_split_nans
+from pymovements.events._utils._filters import filter_candidates_remove_nans \
+    as _filter_candidates_remove_nans
 
 
+@deprecated(
+    reason='This function will be removed in v0.26.0.',
+    version='v0.21.1',
+)
 def filter_candidates_remove_nans(
         candidates: list[np.ndarray],
         values: np.ndarray,
@@ -33,6 +44,9 @@ def filter_candidates_remove_nans(
 
     Removes leading and ending np.nans for all candidates in candidates
 
+    .. deprecated:: v0.21.1
+       This module will be removed in v0.26.0.
+
     Parameters
     ----------
     candidates: list[np.ndarray]
@@ -46,23 +60,16 @@ def filter_candidates_remove_nans(
     list[np.ndarray]
         Returns a filtered list of candidates.
     """
-    values = np.array(values)
-    return_candidates = []
-    for candidate in candidates:
-        if len(candidate) == 0:
-            continue
-        cand_values = values[np.array(candidate)]
-        start_id = 0
-        while np.sum(np.isnan(cand_values[start_id, :])) > 0:
-            start_id += 1
-        end_id = len(cand_values) - 1
-        while np.sum(np.isnan(cand_values[end_id, :])) > 0:
-            end_id -= 1
-        cur_candidate = list(candidate[start_id:end_id + 1])
-        return_candidates.append(np.array(cur_candidate))
-    return return_candidates
+    return _filter_candidates_remove_nans(
+        candidates=candidates,
+        values=values,
+    )
 
 
+@deprecated(
+    reason='This function will be removed in v0.26.0.',
+    version='v0.21.1',
+)
 def events_split_nans(
         candidates: list[np.ndarray],
         values: np.ndarray,
@@ -71,6 +78,9 @@ def events_split_nans(
 
     Splits events if np.nans are within an event
 
+    .. deprecated:: v0.21.1
+       This module will be removed in v0.26.0.
+
     Parameters
     ----------
     candidates: list[np.ndarray]
@@ -84,16 +94,7 @@ def events_split_nans(
     list[np.ndarray]
         Returns a filtered list of candidates.
     """
-    values = np.array(values)
-    return_candidates = []
-    for candidate in candidates:
-        if len(candidate) == 0:
-            continue
-        cur_values = values[np.array(candidate)]
-        nan_candidates = consecutive(arr=np.where(~np.isnan(np.sum(cur_values, axis=1)))[0])
-        cand_list = [
-            np.array(candidate[candidate_indices[0]:candidate_indices[-1] + 1])
-            for candidate_indices in nan_candidates
-        ]
-        return_candidates += cand_list
-    return return_candidates
+    return _events_split_nans(
+        candidates=candidates,
+        values=values,
+    )
diff --git a/tests/unit/events/_utils/_filters_test.py b/tests/unit/events/_utils/_filters_test.py
new file mode 100644
index 000000000..07b64599a
--- /dev/null
+++ b/tests/unit/events/_utils/_filters_test.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2023-2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Test pymovements filters."""
+from __future__ import annotations
+
+import numpy as np
+import pytest
+
+from pymovements.events._utils._filters import events_split_nans
+from pymovements.events._utils._filters import filter_candidates_remove_nans
+
+
+@pytest.mark.parametrize(
+    ('params', 'expected'),
+    [
+        pytest.param(
+            {
+                'candidates': [
+                    [0, 1, 2, 3, 4],
+                    [5, 6, 7, 8],
+                ],
+                'values': np.array([
+                    (np.nan, np.nan), (0, 0),
+                    (0, 0), (0, 0),
+                    (np.nan, np.nan),
+                    (np.nan, np.nan),
+                    (0, 0), (0, 0),
+                    (0, 0),
+                ]),
+            },
+            {'values_filter': [np.array([1, 2, 3]), np.array([6, 7, 8])]},
+            id='test_filters',
+        ),
+        pytest.param(
+            {
+                'candidates': [[0, 1, 2, 3, 4, 5, 6, 7]],
+                'values': np.array([
+                    (0, 0),
+                    (0, 0), (0, 0),
+                    (np.nan, np.nan),
+                    (np.nan, np.nan),
+                    (0, 0), (0, 0),
+                    (0, 0),
+                ]),
+            },
+            {'values_split': [np.array([0, 1, 2]), np.array([5, 6, 7])]},
+            id='test_events_split',
+        ),
+        pytest.param(
+            {'candidates': [[]], 'values': np.array([(0, 0)])},
+            {'values_filter': []},
+            id='test_no_candidates_in_array',
+        ),
+        pytest.param(
+            {'candidates': [[]], 'values': np.array([(np.nan, np.nan)])},
+            {'values_split': []},
+            id='test_no_candidates_in_array_nan',
+        ),
+    ],
+)
+def test_filters(params, expected):
+    if 'values_filter' in expected:
+        results = filter_candidates_remove_nans(
+            params['candidates'],
+            params['values'],
+        )
+        assert np.all(np.array(expected['values_filter']) == np.array(results))
+
+    if 'values_split' in expected:
+        results = events_split_nans(
+            params['candidates'],
+            params['values'],
+        )
+        assert np.all(np.array(expected['values_split']) == np.array(results))
diff --git a/tests/unit/utils/filters_test.py b/tests/unit/utils/filters_test.py
index 7ccaf8101..808ad1daf 100644
--- a/tests/unit/utils/filters_test.py
+++ b/tests/unit/utils/filters_test.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2023-2025 The pymovements Project Authors
+# Copyright (c) 2025 The pymovements Project Authors
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -17,75 +17,49 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-"""Test pymovements filters."""
-from __future__ import annotations
+"""Tests deprecated utils.filters."""
+import re
 
 import numpy as np
 import pytest
 
+from pymovements import __version__
 from pymovements.utils.filters import events_split_nans
 from pymovements.utils.filters import filter_candidates_remove_nans
 
 
-@pytest.mark.parametrize(
-    ('params', 'expected'),
-    [
-        pytest.param(
-            {
-                'candidates': [
-                    [0, 1, 2, 3, 4],
-                    [5, 6, 7, 8],
-                ],
-                'values': np.array([
-                    (np.nan, np.nan), (0, 0),
-                    (0, 0), (0, 0),
-                    (np.nan, np.nan),
-                    (np.nan, np.nan),
-                    (0, 0), (0, 0),
-                    (0, 0),
-                ]),
-            },
-            {'values_filter': [np.array([1, 2, 3]), np.array([6, 7, 8])]},
-            id='test_filters',
-        ),
-        pytest.param(
-            {
-                'candidates': [[0, 1, 2, 3, 4, 5, 6, 7]],
-                'values': np.array([
-                    (0, 0),
-                    (0, 0), (0, 0),
-                    (np.nan, np.nan),
-                    (np.nan, np.nan),
-                    (0, 0), (0, 0),
-                    (0, 0),
-                ]),
-            },
-            {'values_split': [np.array([0, 1, 2]), np.array([5, 6, 7])]},
-            id='test_events_split',
-        ),
-        pytest.param(
-            {'candidates': [[]], 'values': np.array([(0, 0)])},
-            {'values_filter': []},
-            id='test_no_candidates_in_array',
-        ),
-        pytest.param(
-            {'candidates': [[]], 'values': np.array([(np.nan, np.nan)])},
-            {'values_split': []},
-            id='test_no_candidates_in_array_nan',
-        ),
-    ],
-)
-def test_filters(params, expected):
-    if 'values_filter' in expected:
-        results = filter_candidates_remove_nans(
-            params['candidates'],
-            params['values'],
-        )
-        assert np.all(np.array(expected['values_filter']) == np.array(results))
+@pytest.mark.filterwarnings('ignore::DeprecationWarning')
+@pytest.mark.parametrize('filter_function', [events_split_nans, filter_candidates_remove_nans])
+def test_filter_function(filter_function):
+    candidates = [[0, 1], [2, 3]]
+    values = np.array([(np.nan, np.nan), (0, 0), (0, 0), (0, 0)])
 
-    if 'values_split' in expected:
-        results = events_split_nans(
-            params['candidates'],
-            params['values'],
-        )
-        assert np.all(np.array(expected['values_split']) == np.array(results))
+    filter_function(candidates=candidates, values=values)
+
+
+@pytest.mark.parametrize('filter_function', [events_split_nans, filter_candidates_remove_nans])
+def test_filter_function_deprecated(filter_function):
+    candidates = [[0, 1], [2, 3]]
+    values = np.array([(np.nan, np.nan), (0, 0), (0, 0), (0, 0)])
+
+    with pytest.raises(DeprecationWarning):
+        filter_function(candidates=candidates, values=values)
+
+
+@pytest.mark.parametrize('filter_function', [events_split_nans, filter_candidates_remove_nans])
+def test_filter_function_removed(filter_function):
+    candidates = [[0, 1], [2, 3]]
+    values = np.array([(np.nan, np.nan), (0, 0), (0, 0), (0, 0)])
+
+    with pytest.raises(DeprecationWarning) as info:
+        filter_function(candidates=candidates, values=values)
+
+    regex = re.compile(r'.*will be removed in v(?P<version>[0-9]*[.][0-9]*[.][0-9]*)[.)].*')
+
+    msg = info.value.args[0]
+    remove_version = regex.match(msg).groupdict()['version']
+    current_version = __version__.split('+')[0]
+    assert current_version < remove_version, (
+        f'utils/filters.py was planned to be removed in v{remove_version}. '
+        f'Current version is v{current_version}.'
+    )


Reviews:
user: login: SiQube
body: thank you for your PR, it seems to me that you did not 

`git checkout origin/main -b NEWBRANCH` before creating the last few pull requests. doing so makes it much easier for a reviewer to see only the changes that you want to make in a pull request.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:42:12Z
user: login: dkrako
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T17:25:24Z
user: login: SiQube
body: 

![](https://i.fluffy.cc/hc8WTNRNffFv0fTPJDTdFFbsKMCQn5Rz.gif)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T18:36:53Z

Review comments:
diff_hunk: @@ -19,9 +19,9 @@
 # SOFTWARE.
 """Module for parsing input data.
 
-.. deprecated:: v0.21.0
+.. deprecated:: v0.22.0
    Please use gaze.from_asc() instead of utils.parsing.
-   This module will be removed in v0.26.0.
+   This module will be removed in v0.27.0.
path: src/pymovements/utils/parsing.py
user: login: SiQube
body: I think this is the third change to `utils/parsing` I'm seeing in PRs submitted within a day. is there any story to it?
created_at: 2025-03-19T15:39:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,7 +37,7 @@
 @deprecated(
     reason='Please use gaze.from_asc() instead of utils.parsing. '
            'This module will be removed in v0.26.0.',
-    version='v0.21.0',
+    version='v0.21.1',
path: src/pymovements/utils/parsing.py
user: login: SiQube
body: why is `utils/parsing` deprecated in this PR?
created_at: 2025-03-19T15:40:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,7 +37,7 @@
 @deprecated(
     reason='Please use gaze.from_asc() instead of utils.parsing. '
            'This module will be removed in v0.26.0.',
-    version='v0.21.0',
+    version='v0.21.1',
path: src/pymovements/utils/parsing.py
user: login: dkrako
body: Parsing was already deprecated two weeks ago. #976 

What you see here is an autoreplace mistake. I had to change the version for all the current deprecation PRs because the PRs weren't merged for `v0.21.0`, so I had to replace the deprecations with `v0.21.1`.

I didn't notice that it also commited `parsing.py`. Good that you noticed! My brain was on autopilot.

I reverted this change.
created_at: 2025-03-19T17:25:24Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-13T16:14:14Z
body: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/1032?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`9d39fe2`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/9d39fe2952b2047687fc091610bfc54e39644ffe?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) to head [(`6ffb164`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/6ffb164ae32077d6f7e45e83360bd6079501fd84?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).
> Report is 85 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main     #1032   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           81        82    +1     
  Lines         3629      3640   +11     
  Branches       646       646           
=========================================
+ Hits          3629      3640   +11     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/1032?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: dkrako
created_at: 2025-03-19T17:18:49Z
body: > thank you for your PR, it seems to me that you did not
> 
> `git checkout origin/main -b NEWBRANCH` before creating the last few pull requests. doing so makes it much easier for a reviewer to see only the changes that you want to make in a pull request.

I guess you mean the other PRs, as in this PR there is no change from other branches included, but I always branch from main whenever possible.

The thing with the last deprecation PRs is, that there's a dependency (downloads depends on archives depends on paths). If a PR depends on another PR it is not possible to create a branch from main, as the changes from the other PR need to be included.
author_association: MEMBER
user: login: dkrako
created_at: 2025-03-19T17:29:32Z
body: In case of dependencies between PRs I advise you as a reviewer to do the following:
1. review the first PR in the dependency queue.
2. wait until the PR is merged.
3. merge main into the next PR and goto 1. for reviewing the next PR.

Step 2 can take a while (especially if there are changes requested), but that's unavoidable.
author_association: MEMBER