Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- uses: actions/checkout@v4

- name: Build wheels
uses: pypa/cibuildwheel@v2.23.3
env:
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-* cp314-*"
CIBW_SKIP: "*-win32 *_i686 *musllinux*"

- uses: actions/upload-artifact@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v4

- name: Build wheels
uses: pypa/cibuildwheel@v2.23.3
env:
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-* cp314-*"
CIBW_SKIP: "*-win32 *_i686 *musllinux*"

- uses: actions/upload-artifact@v4
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Tests

on:
push:
branches: [master]
pull_request:
workflow_dispatch:

jobs:
test:
name: ${{ matrix.os }} / py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --group dev

# setuptools isn't a runtime/dev dependency, only needed transiently
# to drive build_ext here.
- name: Build C extension in place
run: uv run --with setuptools python setup.py build_ext --inplace

- name: Confirm C extension is importable
shell: bash
run: uv run python -c "import _dbdreader; print('C extension:', _dbdreader.__file__)"

- name: Run tests (C extension)
shell: bash
env:
DBDREADER_C_EXTENSION: "1"
run: uv run pytest tests -v

- name: Run tests (pure-Python fallback)
shell: bash
env:
DBDREADER_C_EXTENSION: "0"
run: uv run pytest tests -v

docs:
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Install dependencies
run: uv sync --group dev

- name: Build Sphinx docs
run: uv run sphinx-build -W -b html docs/source docs/build/html
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ MANIFEST
*.so
# this cac file gets created anyway.
data/cac/daad1b20.cac
uv.lock
dbdreader/data/01600000.dbd
dbdreader/data/02380108.ebd

# pixi things in case user puts those in:
pixi.lock
pixi.toml
.pixi/

.gitattributes
5 changes: 5 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
import os

# Ensure tests always import from the local source tree, not any installed package.
sys.path.insert(0, os.path.dirname(__file__))
39 changes: 36 additions & 3 deletions dbdreader/dbdreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time
import numpy
import glob
import fnmatch
import sys
import re
import datetime
Expand Down Expand Up @@ -407,6 +408,38 @@ def set_cachedir(cls, path, force_makedirs=False):



def _glob(pattern):
''' Case-sensitive glob.

glob.glob() matches filenames case-insensitively on platforms with a
case-insensitive filesystem (notably Windows). For glider data files
this is a problem: lower and upper case extensions (.sbd/.SBD,
.tbd/.TBD, and so on) denote different (full resolution vs.
compact/telemetered) data files and must not be confused. This
wrapper filters glob.glob()'s result so that only names matching the
pattern case-sensitively are kept, giving the same result on every
platform.

Parameters
----------
pattern : str
search pattern, as passed to glob.glob()

Returns
-------
list of str
filenames matching pattern, case-sensitively.
'''
normalised_pattern = os.path.normpath(pattern)
matches = [fn for fn in glob.glob(pattern)
if fnmatch.fnmatchcase(os.path.normpath(fn), normalised_pattern)]
# glob.glob() returns filenames built with the OS-native separator
# (e.g. backslashes on Windows), even when the pattern itself uses
# forward slashes. Normalise to forward slashes so that filenames
# are stable and comparable across platforms.
return [fn.replace(os.sep, '/') for fn in matches]


class DBDList(list):

''' List that properly sorts dbd files.
Expand Down Expand Up @@ -621,7 +654,7 @@ def get_filenames(self, pattern, filenames, cacheDir=None):
if not pattern and not filenames:
raise ValueError("Expected some pattern to search files for or file list.")
if pattern:
all_filenames=DBDList(glob.glob(pattern))
all_filenames=DBDList(_glob(pattern))
elif filenames:
all_filenames=DBDList(filenames)
else:
Expand Down Expand Up @@ -1403,7 +1436,7 @@ def __init__(self,filenames=None,pattern=None,cacheDir=None,complemented_files_o
if filenames:
fns+=filenames
if pattern:
fns+=glob.glob(pattern)
fns+=_glob(pattern)
if len(fns)==0:
raise DbdError(DBD_ERROR_NO_FILES_FOUND)
fns.sort()
Expand Down Expand Up @@ -2028,7 +2061,7 @@ def _refresh_cache(self):
time_limits[1]=min(time_limits[1],time_limits_dataset[1])

def _format_time(self,t,fmt):
tmp = datetime.datetime.fromtimestamp(t, datetime.UTC)
tmp = datetime.datetime.fromtimestamp(t, datetime.timezone.utc)
return tmp.strftime(fmt)

def _get_time_range(self,time_limits,fmt):
Expand Down
18 changes: 13 additions & 5 deletions extension/decompress.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,23 @@ static FILE* fopen_compressed_file_fopen(const char* filename, int* errorno){
fpmem = fopen(base, "rb");
}
else{
/* Decompression failed partway through, so the base file on
disk is incomplete/corrupt. Remove it -- otherwise a later
call would find this partial file, mistake it for a
complete, previously-decompressed file, and silently return
truncated data instead of re-reporting the error. */
remove(base);
fpmem=NULL;
}
}
else {
/* Could not create the base file for writing. This is fatal. */
*errorno = ERROR_FAILED_TO_WRITE_BASE_FILE;
}
}
else {
/* Writing operation failed. This is fatal. */
*errorno = ERROR_FAILED_TO_WRITE_BASE_FILE;
fpmem=NULL;
}
/* else: the decompressed file already exists on disk (written by
a previous call), so just use it as-is -- fpmem is already open
for reading and *errorno is NO_ERROR. */
/* Here fpmem is either NULL (and all failed, or it points to the
decompressed file.*/
free(extension_decompressed);
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ exclude = '''
'''

[tool.mypy]
python_version = "3.13"
python_version = "3.14"
strict = true # Enable all checks
disallow_untyped_calls = true
55 changes: 35 additions & 20 deletions tests/test_decompress.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
from hashlib import md5
from itertools import chain

if not __name__ == '__main__':
from pytest import fixture
Expand All @@ -23,8 +22,8 @@ def load_verification_data():
with open(filename_verify, 'r') as fp:
data = fp.read()
return data
# Open a file and lazy read block by block. Compares with

# Open a file and lazy read block by block. Compares with
# uncompressed file
#
def test_read_file(load_verification_data):
Expand All @@ -36,7 +35,7 @@ def test_read_file(load_verification_data):
data += block
assert data.decode('ascii') == verification_data

# Open a file and decompress whole file. Compares with
# Open a file and decompress whole file. Compares with
# uncompressed file
#
def test_read_file_in_memory(load_verification_data):
Expand All @@ -46,16 +45,16 @@ def test_read_file_in_memory(load_verification_data):
data = d.decompress()
assert data.decode('ascii') == verification_data

# Open a file and decompress first block only.
#
# Open a file and decompress first block only.
#
#
def test_read_file_one_block_only():
filename = 'dbdreader/data/01600000.mcg'
with Decompressor(filename) as d:
blocks = [block for block in d.decompressed_blocks(n=1)]
assert len(blocks) == 1

# Open a file and lazy read block by block. Compares with
# Open a file and lazy read block by block. Compares with
# uncompressed file. Instead of using with statement, open file
# explicitly
def test_read_file_explicit_file_opener(load_verification_data):
Expand All @@ -68,7 +67,7 @@ def test_read_file_explicit_file_opener(load_verification_data):
data += block
assert data.decode('ascii') == verification_data

# Open a file and decompress whole file. Compares with
# Open a file and decompress whole file. Compares with
# uncompressed file. Instead of using with statement, open file
# explicitly
def test_read_file_in_memory_explicit_file_opener(load_verification_data):
Expand Down Expand Up @@ -130,8 +129,8 @@ def test_extension_generator_with_invalid_extension():
s = fd._generate_filename_for_output('01600000.cd')


# Open a file and lazy read block by block. Check cac filesCompares with

# Open a file and lazy read block by block. Check cac filesCompares with
# uncompressed file
#
def test_read_ccc_file():
Expand Down Expand Up @@ -167,8 +166,8 @@ def test_CompressedFileReadlines(load_verification_data):
data = b"".join(lines)
assert data.decode('ascii') == verification_data



# Test reading of data/01600001.dcd and check it is identical to data/01600001.dbd
def test_read_compressed_file_C_code():
compressed_filename = 'dbdreader/data/01600001.dcd'
Expand All @@ -178,7 +177,7 @@ def test_read_compressed_file_C_code():
compressed_data = compressed_dbd.get("m_depth")
regular_data = regular_dbd.get("m_depth")
assert np.all(compressed_data[0]==regular_data[0]) and np.all(compressed_data[1]==regular_data[1])

# Test reading of data/0160000?.dcd MultiDBD
def test_read_compressed_files_C_code():
pattern = 'dbdreader/data/0160000?.dcd'
Expand All @@ -193,7 +192,7 @@ def test_missing_cac_file_for_single_compressed_file():
os.unlink('dbdreader/data/cac/daad1b20.cac')
except:
pass

with pytest.raises(dbdreader.DbdError):
dbd = dbdreader.DBD('dbdreader/data/01600000.ecd', cacheDir='dbdreader/data/cac')

Expand All @@ -203,7 +202,7 @@ def test_missing_cac_file_for_compressed_file_multidbd():
os.unlink('dbdreader/data/cac/daad1b20.cac')
except:
pass

dbd = dbdreader.MultiDBD('dbdreader/data/0160000?.ecd', cacheDir='dbdreader/data/cac')
assert os.path.exists('dbdreader/data/cac/daad1b20.cac')

Expand All @@ -227,7 +226,23 @@ def test_handle_corrupt_compressed_file_multidbd():
data_both = dbd.get("sci_water_temp", "sci_water_cond", continue_on_reading_error=True)
dbd = dbdreader.DBD('dbdreader/data/02380107.ecd', cacheDir='dbdreader/data/cac')
data_one = dbd.get("sci_water_temp", "sci_water_cond")
assert np.all([np.all(x==y) for x, y in zip(chain(*data_both), chain(*data_one))])
labels = [
"time[sci_water_temp]",
"sci_water_temp",
"time[sci_water_cond]",
"sci_water_cond",
]
assert len(data_both) == len(data_one), (
f"Returned tuple length differs: {len(data_both)} vs {len(data_one)}"
)

for idx, (arr_both, arr_one) in enumerate(zip(data_both, data_one)):
label = labels[idx] if idx < len(labels) else f"output[{idx}]"
np.testing.assert_array_equal(
arr_both,
arr_one,
err_msg=f"Mismatch in {label}",
)


# Test handling of files that fail to decompress properly
Expand All @@ -236,15 +251,15 @@ def test_handle_failing_decompression_multidbd():
t, T = dbd.get("sci_water_temp")
# we should get 126 values
assert len(t) == 126


# Test handling of files that fail to decompress properly
def test_handle_failing_decompression_dbd():
# when called with a DBD instance we expect an error to be raised.
with pytest.raises(dbdreader.DbdError) as e:
dbd = dbdreader.DBD('dbdreader/data/02450137.tcd', cacheDir='dbdreader/data/cac')
assert e.value.value == dbdreader.DBD_ERROR_DECOMPRESSION_ERROR







Loading