From 41ee8336f9336755117076d432ad849d826498d2 Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:03:59 -0500 Subject: [PATCH 1/6] Initial setup for allow_duplicates in readers --- src/wristpy/core/models.py | 5 +++ src/wristpy/io/readers/readers.py | 29 +++++++++++++- tests/unit/test_readers.py | 64 +++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/wristpy/core/models.py b/src/wristpy/core/models.py index ae6c7f73..98aabc68 100644 --- a/src/wristpy/core/models.py +++ b/src/wristpy/core/models.py @@ -96,6 +96,11 @@ def validate_time(cls, v: pl.Series) -> pl.Series: if not isinstance(v.dtype, pl.datatypes.Datetime): raise ValueError("Time must be a datetime series") if not v.is_unique().all(): + logger.warning( + "Duplicate timestamps found in time series. " + "See the `allow_duplicates` parameter if you " + "would want to process this data regardless." + ) raise ValueError("Time series must contain unique entries") if not v.is_sorted(): raise ValueError("Time series must be sorted") diff --git a/src/wristpy/io/readers/readers.py b/src/wristpy/io/readers/readers.py index dc5c25df..4072f621 100644 --- a/src/wristpy/io/readers/readers.py +++ b/src/wristpy/io/readers/readers.py @@ -7,18 +7,31 @@ import numpy as np import polars as pl -from wristpy.core import models +from wristpy.core import config, models +logger = config.get_logger() -def read_watch_data(file_name: Union[pathlib.Path, str]) -> models.WatchData: + +def read_watch_data( + file_name: Union[pathlib.Path, str], allow_duplicates: bool = False +) -> models.WatchData: """Read watch data from a file. Currently supported watch types are Actigraph .gt3x and GeneActiv .bin. Assigns the idle_sleep_mode_flag to false unless the watchtype is .gt3x and sleep_mode is enabled (based on watch metadata). + If requested, removes duplicate timestamps from the data, keeping only unique + timestamps and their corresponding sensor values. + Args: file_name: The filename to read the watch data from. + allow_duplicates: Whether to allow duplicate timestamps in the data. If + False, duplicate timestamps will raise a ValueError in the + Measurement validation phase. If set to True, we will keep only the + unique timestamps and the associated sensor values. The first occurrence + of each timestamp is kept. + Default is False. Returns: WatchData class @@ -39,7 +52,19 @@ def read_watch_data(file_name: Union[pathlib.Path, str]) -> models.WatchData: for timeseries in data["timeseries"].values(): time = unix_epoch_time_to_polars_datetime(timeseries["datetime"]) + if allow_duplicates: + logger.info( + "Keeping only unique timestamps as requested. " + "Please note that there may have been duplicate timestamps, " + "which is indicative of sensor malfunction." + ) + unique_time_indices = time.arg_unique() + time = time.gather(unique_time_indices) + for sensor_name, sensor_values in timeseries.items(): + if allow_duplicates: + sensor_values = sensor_values[unique_time_indices.to_numpy()] + measurements[sensor_name] = models.Measurement( measurements=sensor_values, time=time ) diff --git a/tests/unit/test_readers.py b/tests/unit/test_readers.py index 3cb3baa6..faebb53a 100644 --- a/tests/unit/test_readers.py +++ b/tests/unit/test_readers.py @@ -4,6 +4,7 @@ from typing import Literal, cast import actfast +import numpy as np import pytest from wristpy.core import models @@ -94,3 +95,66 @@ def test_timezone_extraction_bin(sample_data_bin: pathlib.Path) -> None: assert ( watch_data.time_zone == expected_timezone ), f"Expected timezone of: {expected_timezone}, result was: {watch_data.time_zone}" + + +@pytest.mark.parametrize( + "file_type,mock_data", + [ + ( + ".gt3x", + { + "timeseries": { + "acceleration": { + "datetime": np.array([1000000000, 1000000000, 2000000000]), + "acceleration": np.array([[1, 2, 3], [2, 3, 4], [4, 5, 6]]), + }, + "light": { + "datetime": np.array([1000000000, 1000000000, 2000000000]), + "light": np.array([10, 20, 30]), + }, + }, + "metadata": { + "device_feature_enabled": {"sleep_mode": "false"}, + "info": { + "TimeZone": "-05:00:00", + "Acceleration Min": "-8", + "Acceleration Max": "8", + }, + }, + }, + ), + ( + ".bin", + { + "timeseries": { + "high_frequency": { + "datetime": np.array([1000000000, 1000000000, 2000000000]), + "acceleration": np.array([[1, 2, 3], [2, 3, 4], [4, 5, 6]]), + }, + "low_frequency": { + "datetime": np.array([1000000000, 1000000000, 2000000000]), + "light": np.array([10, 20, 30]), + }, + }, + "metadata": { + "Device Capabilities": {"Accelerometer Range": "-8 to 8"}, + "Configuration Info": {"Time Zone": "UTC-05:00"}, + }, + }, + ), + ], +) +def test_allow_duplicates_option( + mocker: pytest.MonkeyPatch, file_type: str, mock_data: dict +) -> None: + """Test the allow_duplicates option in read_watch_data function.""" + mocker.patch("actfast.read", return_value=mock_data) + + watch_data = readers.read_watch_data(f"dummy{file_type}", allow_duplicates=True) + + assert len(watch_data.acceleration.time) == 2 + assert len(watch_data.lux.time) == 2 + assert np.array_equal( + watch_data.acceleration.measurements, np.array([[1, 2, 3], [4, 5, 6]]) + ) + assert np.array_equal(watch_data.lux.measurements, np.array([10, 30])) From f5a2eefdd9f6e775ae123f42764abf3ebde96317 Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:40:37 -0500 Subject: [PATCH 2/6] Add cli/entrypoint for allow_duplicates --- pyproject.toml | 2 +- src/wristpy/core/cli.py | 13 +++++++++++++ src/wristpy/core/orchestrator.py | 14 +++++++++++++- tests/unit/test_cli.py | 6 ++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27c28168..daa394bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wristpy" -version = "0.2.2" +version = "0.2.3" description = "wristpy is a Python package designed for processing and analyzing wrist-worn accelerometer data." authors = [ "Adam Santorelli ", diff --git a/src/wristpy/core/cli.py b/src/wristpy/core/cli.py index 51f31153..68c97455 100644 --- a/src/wristpy/core/cli.py +++ b/src/wristpy/core/cli.py @@ -153,6 +153,18 @@ def main( "Must be greater than or equal to 1.", min=1, ), + allow_duplicates: bool = typer.Option( + False, + "-d", + "--allow-duplicates", + help="Whether to allow duplicate timestamps in the sensor data. " + "If this flag is set, no error will be raised during Measurement validation and " + "processing can continue. Only unique timestamps and their corresponding " + "sensor values will be kept. " + "The first occurrence of each timestamp is retained. Defaults to False. " + "Note that the presence of duplicate timestamps may indicate sensor " + "malfunction. Modifying this parameter should be done with caution.", + ), verbosity: bool = typer.Option( False, "-v", @@ -194,6 +206,7 @@ def main( nonwear_algorithm=nonwear_algorithms, # type: ignore[arg-type] # Covered by NonwearAlgorithm Enum class verbosity=log_level, output_filetype=output_filetype.value, + allow_duplicates=allow_duplicates, ) except exceptions.EmptyDirectoryError as e: typer.echo(f"Error: {e}", err=True) diff --git a/src/wristpy/core/orchestrator.py b/src/wristpy/core/orchestrator.py index 95eb5ce2..40025057 100644 --- a/src/wristpy/core/orchestrator.py +++ b/src/wristpy/core/orchestrator.py @@ -41,6 +41,7 @@ def run( epoch_length: float = 5, activity_metric: Sequence[Literal["enmo", "mad", "ag_count", "mims"]] = ["enmo"], nonwear_algorithm: Sequence[Literal["ggir", "cta", "detach"]] = ["ggir"], + allow_duplicates: bool = False, verbosity: int = logging.WARNING, output_filetype: Literal[".csv", ".parquet"] = ".csv", ) -> Union[writers.OrchestratorResults, Dict[str, writers.OrchestratorResults]]: @@ -70,6 +71,7 @@ def run( activity_metric: The metric(s) to be used for physical activity categorization. Multiple metrics can be specified as a sequence. nonwear_algorithm: The algorithm to be used for nonwear detection. + allow_duplicates: Whether to allow duplicate timestamps in the sensor data. verbosity: The logging level for the logger. output_filetype: Specifies the data format for the save files. Only used when processing directories. @@ -133,6 +135,7 @@ def run( activity_metric=activity_metric, verbosity=verbosity, nonwear_algorithm=nonwear_algorithm, + allow_duplicates=allow_duplicates, ) return _run_directory( @@ -145,6 +148,7 @@ def run( verbosity=verbosity, output_filetype=output_filetype, nonwear_algorithm=nonwear_algorithm, + allow_duplicates=allow_duplicates, ) @@ -161,6 +165,7 @@ def _run_directory( verbosity: int = logging.WARNING, output_filetype: Literal[".csv", ".parquet"] = ".csv", activity_metric: Sequence[Literal["enmo", "mad", "ag_count", "mims"]] = ["enmo"], + allow_duplicates: bool = False, ) -> Dict[str, writers.OrchestratorResults]: """Runs main processing steps for wristpy on directories. @@ -186,6 +191,7 @@ def _run_directory( output_filetype: Specifies the data format for the save files. activity_metric: The metric(s) to be used for physical activity categorization. Multiple metrics can be specified as a sequence. + allow_duplicates: Whether to allow duplicate timestamps in the sensor data. Returns: All calculated data in a save ready format as a dictionary of @@ -256,6 +262,7 @@ def _run_directory( verbosity=verbosity, nonwear_algorithm=nonwear_algorithm, activity_metric=activity_metric, + allow_duplicates=allow_duplicates, ) except Exception as e: logger.error("Did not run file: %s, Error: %s", file, e) @@ -275,6 +282,7 @@ def _run_file( epoch_length: float = 5.0, activity_metric: Sequence[Literal["enmo", "mad", "ag_count", "mims"]] = ["enmo"], nonwear_algorithm: Sequence[Literal["ggir", "cta", "detach"]] = ["ggir"], + allow_duplicates: bool = False, verbosity: int = logging.WARNING, ) -> writers.OrchestratorResults: """Runs main processing steps for wristpy and returns data for analysis. @@ -303,6 +311,10 @@ def _run_file( Multiple metrics can be specified as a sequence. nonwear_algorithm: The algorithm to be used for nonwear detection. A sequence of algorithms can be provided. If so, a majority vote will be taken. + allow_duplicates: Whether to allow duplicate timestamps in the sensor data. + If set to True, no error will be raised and we will keep only the unique + timestamps and their associated sensor values. The first occurrence of each + timestamp is kept. verbosity: The logging level for the logger. Returns: @@ -346,7 +358,7 @@ def _run_file( logger.error(msg) raise ValueError(msg) - watch_data = readers.read_watch_data(input) + watch_data = readers.read_watch_data(input, allow_duplicates=allow_duplicates) if calibrator is None: logger.debug("Running without calibration") diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index a1877b4f..e40fe9b2 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -36,6 +36,7 @@ def test_main_default( activity_metric=["enmo"], epoch_length=5, nonwear_algorithm=["ggir"], + allow_duplicates=False, verbosity=logging.INFO, output_filetype=".csv", ) @@ -69,6 +70,7 @@ def test_main_with_metrics( activity_metric=[metric], nonwear_algorithm=["ggir"], epoch_length=5, + allow_duplicates=False, verbosity=logging.INFO, ) @@ -93,6 +95,7 @@ def test_main_with_multiple_metrics( nonwear_algorithm=["ggir"], epoch_length=5, verbosity=logging.INFO, + allow_duplicates=False, ) @@ -125,6 +128,7 @@ def test_main_with_options( "cta", "-n", "ggir", + "-d", ] ), ) @@ -139,6 +143,7 @@ def test_main_with_options( epoch_length=3, verbosity=logging.INFO, output_filetype=".csv", + allow_duplicates=True, ) @@ -191,6 +196,7 @@ def test_main_verbosity( nonwear_algorithm=["ggir"], verbosity=logging.DEBUG, output_filetype=".csv", + allow_duplicates=False, ) From 5ea8bb4852af63f1aa691d0c180f7749221d5ede Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:44:37 -0500 Subject: [PATCH 3/6] Update poetry.lock Bump for security --- poetry.lock | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9baef807..2beb6ff5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "actfast" @@ -58,7 +58,7 @@ version = "4.11.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, @@ -609,7 +609,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev", "docs"] -markers = "python_version == \"3.10\"" +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -653,14 +653,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.3" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, - {file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, + {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, + {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, ] [[package]] @@ -698,8 +698,6 @@ files = [ {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, @@ -709,8 +707,6 @@ files = [ {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, @@ -720,8 +716,6 @@ files = [ {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, @@ -731,8 +725,6 @@ files = [ {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, @@ -740,8 +732,6 @@ files = [ {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, - {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, - {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, @@ -751,8 +741,6 @@ files = [ {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, @@ -795,7 +783,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -2706,7 +2694,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -3078,7 +3066,7 @@ version = "0.49.1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875"}, {file = "starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb"}, @@ -3159,7 +3147,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] -markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version == \"3.10\""} +markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version < \"3.11\""} [[package]] name = "tornado" @@ -3250,7 +3238,7 @@ version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, @@ -3284,19 +3272,19 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3) [[package]] name = "virtualenv" -version = "20.34.0" +version = "20.36.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, - {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, + {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, + {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" +filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} From 458f30d739884a1c05ddffa65e0feeb1380576ca Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:02:59 -0500 Subject: [PATCH 4/6] Fixing mypy/ruff --- src/wristpy/core/cli.py | 4 ++-- tests/unit/test_readers.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/wristpy/core/cli.py b/src/wristpy/core/cli.py index 68c97455..71221291 100644 --- a/src/wristpy/core/cli.py +++ b/src/wristpy/core/cli.py @@ -158,8 +158,8 @@ def main( "-d", "--allow-duplicates", help="Whether to allow duplicate timestamps in the sensor data. " - "If this flag is set, no error will be raised during Measurement validation and " - "processing can continue. Only unique timestamps and their corresponding " + "If this flag is set, no error will be raised during Measurement validation " + "and processing can continue. Only unique timestamps and their corresponding " "sensor values will be kept. " "The first occurrence of each timestamp is retained. Defaults to False. " "Note that the presence of duplicate timestamps may indicate sensor " diff --git a/tests/unit/test_readers.py b/tests/unit/test_readers.py index faebb53a..65ad876c 100644 --- a/tests/unit/test_readers.py +++ b/tests/unit/test_readers.py @@ -6,6 +6,7 @@ import actfast import numpy as np import pytest +import pytest_mock from wristpy.core import models from wristpy.io.readers import readers @@ -145,13 +146,14 @@ def test_timezone_extraction_bin(sample_data_bin: pathlib.Path) -> None: ], ) def test_allow_duplicates_option( - mocker: pytest.MonkeyPatch, file_type: str, mock_data: dict + mocker: pytest_mock.MockFixture, file_type: str, mock_data: dict ) -> None: """Test the allow_duplicates option in read_watch_data function.""" mocker.patch("actfast.read", return_value=mock_data) watch_data = readers.read_watch_data(f"dummy{file_type}", allow_duplicates=True) + assert watch_data.lux is not None assert len(watch_data.acceleration.time) == 2 assert len(watch_data.lux.time) == 2 assert np.array_equal( From 17c663c851199081394993abd7f0666c712441b1 Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:09:25 -0500 Subject: [PATCH 5/6] Fixed some typos --- tests/unit/test_calibration.py | 4 ++-- tests/unit/test_mims.py | 4 ++-- tests/unit/test_orchestrator.py | 2 +- tests/unit/test_processing_utils.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_calibration.py b/tests/unit/test_calibration.py index 35399189..3d7fd44c 100644 --- a/tests/unit/test_calibration.py +++ b/tests/unit/test_calibration.py @@ -28,7 +28,7 @@ def create_dummy_measurement( Returns: A Measurement object with dummy data for testing. The data will be either all a single value for trivial cases, or normalized random data depending on the - arguements given. + arguments given. """ n_samples = int(sampling_rate * 3600 * duration_hours) start_time = datetime(2024, 5, 4, 12, 0, 0) @@ -94,7 +94,7 @@ def test_sphere_error() -> None: def test_zero_scale_error() -> None: - """Test error due to scale becomeing zero values.""" + """Test error due to scale becoming zero values.""" data = np.array([[2.0, 2.0, 2.0], [2.0, 2.0, 2.0], [-0.001, -0.001, -0.001]]) calibrator = calibration.GgirCalibration(min_acceleration=0) diff --git a/tests/unit/test_mims.py b/tests/unit/test_mims.py index c346ce2f..fe80b15b 100644 --- a/tests/unit/test_mims.py +++ b/tests/unit/test_mims.py @@ -94,7 +94,7 @@ def test_interpolate_data( def test_extrapolate_points() -> None: - """Test the succesful running of extrapolate points.""" + """Test the successful running of extrapolate points.""" test_data = create_clipped_sine_data() test_time = create_sine_data_time_stamps() test_measure = models.Measurement(measurements=test_data, time=test_time) @@ -455,7 +455,7 @@ def test_fit_weighted_insufficient_data() -> None: def test_fit_weighted_out_of_range() -> None: - """Test that _fit_weighted returns None when recieving -1 indices(out of range).""" + """Test that _fit_weighted returns None when receiving -1 indices(out of range).""" axis = np.arange(10, dtype=float) time_numeric = np.arange(10, dtype=float) marker = np.zeros(10) diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py index 45a69c26..e185a2b8 100644 --- a/tests/unit/test_orchestrator.py +++ b/tests/unit/test_orchestrator.py @@ -79,7 +79,7 @@ def test_save_results( def test_validate_output_invalid_file_type(tmp_path: pathlib.Path) -> None: - """Test when a bad extention is given.""" + """Test when a bad extension is given.""" with pytest.raises(exceptions.InvalidFileTypeError): writers.OrchestratorResults.validate_output(tmp_path / "bad_file.oops") diff --git a/tests/unit/test_processing_utils.py b/tests/unit/test_processing_utils.py index 4a1ae5b3..1ebd8ab3 100644 --- a/tests/unit/test_processing_utils.py +++ b/tests/unit/test_processing_utils.py @@ -90,7 +90,7 @@ def test_nonwear_dispatcher_default() -> None: def test_nonwear_dispatcher_multiple() -> None: - """Tests nonwear dispatcher with multiple nonwear alogirhtms requested.""" + """Tests nonwear dispatcher with multiple nonwear algorithms requested.""" num_samples = int(1e5) time_list = [ datetime(2024, 5, 2) + timedelta(milliseconds=100 * i) From c56844a1d6d64863e25b8472dab141a424318532 Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:10:37 -0500 Subject: [PATCH 6/6] Modify tests to use fixture --- src/wristpy/core/models.py | 2 +- src/wristpy/io/readers/readers.py | 2 +- tests/unit/test_readers.py | 24 ++++++++++++++++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/wristpy/core/models.py b/src/wristpy/core/models.py index 98aabc68..faefd855 100644 --- a/src/wristpy/core/models.py +++ b/src/wristpy/core/models.py @@ -96,7 +96,7 @@ def validate_time(cls, v: pl.Series) -> pl.Series: if not isinstance(v.dtype, pl.datatypes.Datetime): raise ValueError("Time must be a datetime series") if not v.is_unique().all(): - logger.warning( + logger.error( "Duplicate timestamps found in time series. " "See the `allow_duplicates` parameter if you " "would want to process this data regardless." diff --git a/src/wristpy/io/readers/readers.py b/src/wristpy/io/readers/readers.py index 4072f621..5c81a43c 100644 --- a/src/wristpy/io/readers/readers.py +++ b/src/wristpy/io/readers/readers.py @@ -13,7 +13,7 @@ def read_watch_data( - file_name: Union[pathlib.Path, str], allow_duplicates: bool = False + file_name: Union[pathlib.Path, str], *, allow_duplicates: bool = False ) -> models.WatchData: """Read watch data from a file. diff --git a/tests/unit/test_readers.py b/tests/unit/test_readers.py index 65ad876c..6802474b 100644 --- a/tests/unit/test_readers.py +++ b/tests/unit/test_readers.py @@ -98,9 +98,8 @@ def test_timezone_extraction_bin(sample_data_bin: pathlib.Path) -> None: ), f"Expected timezone of: {expected_timezone}, result was: {watch_data.time_zone}" -@pytest.mark.parametrize( - "file_type,mock_data", - [ +@pytest.fixture( + params=[ ( ".gt3x", { @@ -145,10 +144,16 @@ def test_timezone_extraction_bin(sample_data_bin: pathlib.Path) -> None: ), ], ) +def duplicate_data_scenarios(request: pytest.FixtureRequest) -> tuple[str, dict]: + """Fixture providing test data with duplicate timestamps.""" + return request.param + + def test_allow_duplicates_option( - mocker: pytest_mock.MockFixture, file_type: str, mock_data: dict + mocker: pytest_mock.MockFixture, duplicate_data_scenarios: tuple[str, dict] ) -> None: """Test the allow_duplicates option in read_watch_data function.""" + file_type, mock_data = duplicate_data_scenarios mocker.patch("actfast.read", return_value=mock_data) watch_data = readers.read_watch_data(f"dummy{file_type}", allow_duplicates=True) @@ -160,3 +165,14 @@ def test_allow_duplicates_option( watch_data.acceleration.measurements, np.array([[1, 2, 3], [4, 5, 6]]) ) assert np.array_equal(watch_data.lux.measurements, np.array([10, 30])) + + +def test_allow_duplicates_false( + mocker: pytest_mock.MockFixture, duplicate_data_scenarios: tuple[str, dict] +) -> None: + """Test the allow_duplicates=False option in read_watch_data function.""" + file_type, mock_data = duplicate_data_scenarios + mocker.patch("actfast.read", return_value=mock_data) + + with pytest.raises(ValueError, match="Time series must contain unique entries"): + readers.read_watch_data(f"dummy{file_type}", allow_duplicates=False)