Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3503808
{Search for wildcards function updated}
Diya910 Apr 13, 2025
054a98b
Refactoring changes asked by the maintainer which include centralisat…
Diya910 Jun 15, 2025
69699d9
Fabrication of the code moved functions in folders.py
Diya910 Jul 2, 2025
15f8a3c
Update datashuttle/utils/folders.py
Diya910 Jul 4, 2025
8104024
Update datashuttle/utils/folders.py
Diya910 Jul 4, 2025
8a2cbbd
Update datashuttle/utils/folders.py
Diya910 Jul 4, 2025
a44cb61
Update datashuttle/utils/folders.py
Diya910 Jul 4, 2025
f9a21b4
Update datashuttle/utils/validation.py
Diya910 Jul 4, 2025
bd12cd6
Refactor: Clean up docstrings in folders.py and canonical_tags.py as …
Diya910 Jul 4, 2025
e0d2441
Merge branch 'date_feature' of https://github.com/Diya910/datashuttle…
Diya910 Jul 4, 2025
cc5f253
Merge remote-tracking branch 'upstream/main' into date_feature
JoeZiminski Jul 30, 2025
d1ede1d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 30, 2025
399b402
Fix linting.
JoeZiminski Jul 30, 2025
d318fb5
Merge branch 'date_feature' of https://github.com/Diya910/datashuttle…
JoeZiminski Jul 30, 2025
ac95503
Properly ignore mypy on tests.
JoeZiminski Jul 31, 2025
f70b6ff
Merge branch 'neuroinformatics-unit:main' into date_feature
Diya910 Aug 12, 2025
9391982
Updated test cases to use basemodel and also added more test cases to…
Diya910 Aug 14, 2025
23679eb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 14, 2025
2e3bcd7
Update datashuttle/utils/folders.py
Diya910 Aug 26, 2025
d4c7187
Merge remote-tracking branch 'upstream/main' into date_feature
JoeZiminski Sep 2, 2025
e2940b2
Cover DATETO etc. cases in check_and_format_names so they are not val…
JoeZiminski Sep 9, 2025
407afe5
Merge branch 'neuroinformatics-unit:main' into date_feature
Diya910 Sep 10, 2025
4c1bd4a
Introduce get_datetime_to_search_regexp function.
JoeZiminski Oct 27, 2025
84524ec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 27, 2025
7da6305
Introduce get_datetime_to_search_regexp function.
JoeZiminski Oct 27, 2025
444a081
Merge branch 'date_feature' of https://github.com/Diya910/datashuttle…
JoeZiminski Oct 27, 2025
c60b5ff
Remove 'test_simple_wildcard_first'.
JoeZiminski Oct 27, 2025
1893fba
Update 'test_datetime_range_transfer'.
JoeZiminski Oct 27, 2025
120c4d6
Use parameterisation for download test.
JoeZiminski Oct 27, 2025
94161e2
Update error message check.
JoeZiminski Oct 27, 2025
0a936a2
Add 'upload_or_download' for all key tests.
JoeZiminski Oct 27, 2025
9d1fe46
Extend test_subject_level_date_range.
JoeZiminski Oct 27, 2025
86107d0
Update test_edge_case_exact_boundary_dates.
JoeZiminski Oct 27, 2025
c49403f
Adjust date test.
JoeZiminski Oct 27, 2025
08d77d8
Small tidy ups.
JoeZiminski Oct 27, 2025
8d9d96b
Troubleshooting.
JoeZiminski Oct 27, 2025
f393aa8
Merge remote-tracking branch 'upstream/main' into date_feature
JoeZiminski Oct 27, 2025
2e45e2b
Support datetime directly as sub ses values @DATETIME@.
JoeZiminski Oct 27, 2025
cc602a8
Add tests for new sub-@DATE@ ses-@DATE@ behaviour.
JoeZiminski Oct 27, 2025
8416bbe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 27, 2025
06933ac
Extend tests to new sub case + some refactoring.
JoeZiminski Oct 27, 2025
28335db
Fix docstring for linter.
JoeZiminski Oct 29, 2025
d42d927
Handle sub case for datetime, date, time.
JoeZiminski Oct 30, 2025
2faf0a2
Merge branch 'date_feature' of https://github.com/Diya910/datashuttle…
JoeZiminski Oct 30, 2025
dc47915
Merge remote-tracking branch 'upstream/main' into date_feature
JoeZiminski Nov 3, 2025
b59eb2f
Fix tests.
JoeZiminski Nov 4, 2025
a376d2c
Update tests/test_date_search_range.py
JoeZiminski May 24, 2026
cd16ce4
Update datashuttle/utils/formatting.py
JoeZiminski May 24, 2026
6231b9f
Update tests/test_date_search_range.py
JoeZiminski May 24, 2026
46e2ccc
Update datashuttle/utils/folders.py
JoeZiminski May 24, 2026
0542d68
Update datashuttle/utils/folders.py
JoeZiminski May 24, 2026
08554d4
Update tests/test_date_search_range.py
JoeZiminski May 24, 2026
43b8141
Update tests/test_date_search_range.py
JoeZiminski May 24, 2026
292be37
Update tests/test_date_search_range.py
JoeZiminski May 24, 2026
83cd0b5
Merge branch 'main' into date_feature
JoeZiminski May 24, 2026
1c8e7f7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 24, 2026
01951e3
Revert copilot commit
JoeZiminski May 24, 2026
a6f287b
Fix outdated api call in tests.
JoeZiminski May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions datashuttle/utils/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
from datashuttle.utils.custom_types import TopLevelFolder

import glob
import re
from datetime import datetime
from pathlib import Path

from datashuttle.configs import canonical_folders, canonical_tags
from datashuttle.utils import ssh, utils, validation
from datashuttle.utils.custom_exceptions import NeuroBlueprintError
from datashuttle.utils.utils import get_values_from_bids_formatted_name

# -----------------------------------------------------------------------------
# Create Folders
Expand Down Expand Up @@ -401,27 +404,65 @@ def search_for_wildcards(
"""
new_all_names: List[str] = []
for name in all_names:
if canonical_tags.tags("*") in name:
Comment thread
Diya910 marked this conversation as resolved.
name = name.replace(canonical_tags.tags("*"), "*")

matching_names: List[str]
if canonical_tags.tags("*") in name or "@DATETO@" in name:
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
search_str = name.replace(canonical_tags.tags("*"), "*")
# If a date-range tag is present, extract dates and update the search string.
if "@DATETO@" in name:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a canonical tags.tags() function that contains all the tags (just in case we change them or some other problem that requires their editing arises). So @DATETO@, @TIMETO@ and @DATETIMETO@ could be added to that function and here @DATETO@ replaced with tags.tags("DATETO")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added DATETO, TIMETO, and DATETIMETO to canonical_tags.py and using them through tags()

m = re.search(r"(\d{8})@DATETO@(\d{8})", name)
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
if not m:
raise ValueError(
"Invalid date range format in name: " + name
)
start_str, end_str = m.groups()
try:
start_date = datetime.strptime(start_str, "%Y%m%d")
end_date = datetime.strptime(end_str, "%Y%m%d")
except ValueError as e:
raise ValueError("Invalid date in date range: " + str(e))
# Replace the date-range substring with "date-*"
search_str = re.sub(r"\d{8}@DATETO@\d{8}", "date-*", name)
# Use the helper function to perform the glob search.
if sub:
matching_names = search_sub_or_ses_level( # type: ignore
cfg, base_folder, local_or_central, sub, search_str=name
matching_names: List[str] = search_sub_or_ses_level(
cfg,
base_folder,
local_or_central,
sub,
search_str=search_str,
)[0]
else:
matching_names = search_sub_or_ses_level( # type: ignore
cfg, base_folder, local_or_central, search_str=name
matching_names = search_sub_or_ses_level(
cfg, base_folder, local_or_central, search_str=search_str
)[0]

# If a date-range tag was provided, further filter the results.
if "@DATETO@" in name:
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
filtered_names: List[str] = []
for candidate in matching_names:
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
candidate_basename = (
candidate
if isinstance(candidate, str)
else candidate.name
)
values_list = get_values_from_bids_formatted_name(
[candidate_basename], "date"
)
if not values_list:
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
continue
candidate_date_str = values_list[0]
try:
candidate_date = datetime.strptime(
candidate_date_str, "%Y%m%d"
)
except ValueError:
continue
if start_date <= candidate_date <= end_date:
filtered_names.append(candidate)
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
matching_names = filtered_names
new_all_names += matching_names
else:
new_all_names += [name]

new_all_names = list(
set(new_all_names)
) # remove duplicate names in case of wildcard overlap

# Remove duplicates in case of wildcard overlap.
new_all_names = list(set(new_all_names))
return new_all_names


Expand All @@ -440,7 +481,7 @@ def search_sub_or_ses_level(
search_str: str = "*",
verbose: bool = True,
return_full_path: bool = False,
) -> Tuple[List[str] | List[Path], List[str]]:
) -> Tuple[Union[List[str], List[Path]], List[str]]:
"""
Search project folder at the subject or session level.
Only returns folders
Expand Down
2 changes: 1 addition & 1 deletion datashuttle/utils/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ def replace_tags_in_regexp(regexp: str) -> str:
Note `replace_date_time_tags_in_name()` operates in place on a list.
"""
regexp_list = [regexp]
date_regexp = "\d\d\d\d\d\d\d\d"
date_regexp = r"\d{8}"
time_regexp = "\d\d\d\d\d\d"

formatting.replace_date_time_tags_in_name(
Expand Down
134 changes: 134 additions & 0 deletions tests/test_date_search_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import glob
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
import os
import re
import shutil
import tempfile
from pathlib import Path
from typing import List

import pytest

from datashuttle.utils.folders import search_for_wildcards


# Dummy implementation for canonical_tags
class DummyCanonicalTags:
@staticmethod
def tags(x: str) -> str:
if x == "*":
return "@*@"
return x


# Patch canonical_tags so that tags("*") returns "@*@"
@pytest.fixture(autouse=True)
def patch_canonical_tags(monkeypatch):
from datashuttle.configs import canonical_tags

monkeypatch.setattr(canonical_tags, "tags", DummyCanonicalTags.tags)


# Dummy implementation for search_sub_or_ses_level that simply performs globbing.
def dummy_search_sub_or_ses_level(
cfg, base_folder: Path, local_or_central: str, *args, search_str: str
):
pattern = os.path.join(str(base_folder), search_str)
matches: List[str] = sorted(glob.glob(pattern))
return (matches,)


# Patch search_sub_or_ses_level in the module where search_for_wildcards is defined.
@pytest.fixture(autouse=True)
def patch_search_sub_or_ses_level(monkeypatch):
monkeypatch.setattr(
"datashuttle.utils.folders.search_sub_or_ses_level",
dummy_search_sub_or_ses_level,
)


# Dummy implementation for get_values_from_bids_formatted_name.
def dummy_get_values_from_bids_formatted_name(name: str, key: str) -> dict:
# Expect name format: "sub-01_date-YYYYMMDD"
m = re.search(r"date-(\d{8})", name)
if m:
return {key: m.group(1)}
return {}


# Patch get_values_from_bids_formatted_name.
@pytest.fixture(autouse=True)
def patch_get_values_from_bids(monkeypatch):
monkeypatch.setattr(
"datashuttle.utils.utils.get_values_from_bids_formatted_name",
dummy_get_values_from_bids_formatted_name,
)


# Fixture to create a temporary directory with a simulated folder structure.
@pytest.fixture
def temp_project_dir() -> Path: # type: ignore
temp_dir = Path(tempfile.mkdtemp())
# Create folders with names in the format "sub-01_date-YYYYMMDD"
folder_dates = [
"20250305",
"20250306",
"20250307",
"20250308",
"20250309",
"20250310",
]
for date_str in folder_dates:
folder_name = f"sub-01_date-{date_str}"
os.mkdir(temp_dir / folder_name)
yield temp_dir
shutil.rmtree(temp_dir)


def test_date_range_wildcard(temp_project_dir: Path):
"""
When given a date-range wildcard pattern like "sub-01_20250306@DATETO@20250309",
only folders whose embedded date falls between 20250306 and 20250309 (inclusive)
should be returned.
"""

class Configs:
pass

cfg = Configs()
base_folder = temp_project_dir
local_or_central = "local"
pattern = "sub-01_20250306@DATETO@20250309"
result = search_for_wildcards(
cfg, base_folder, local_or_central, [pattern]
)

# Extract the dates from the returned folder names.
found_dates = set()
for folder in result:
basename = os.path.basename(folder)
m = re.search(r"date-(\d{8})", basename)
if m:
found_dates.add(m.group(1))

expected_dates = {"20250306", "20250307", "20250308", "20250309"}
assert found_dates == expected_dates


def test_simple_wildcard(temp_project_dir: Path):
Comment thread
JoeZiminski marked this conversation as resolved.
Outdated
"""
When given a simple wildcard pattern like "sub-01_@*@",
all folders should be returned.
"""

class Configs:
pass

cfg = Configs()
base_folder = temp_project_dir
local_or_central = "local"
pattern = "sub-01_@*@"
result = search_for_wildcards(
cfg, base_folder, local_or_central, [pattern]
)
# We expect six folders.
assert len(result) == 6