Skip to content
Open
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
28 changes: 27 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pip install hecdss
[API Documentation](https://hydrologicengineeringcenter.github.io/hec-dss-python/)

### DSS file methods
1. `HecDss(file_path: str)`: Opens a DSS file located at the provided file path.
1. `HecDss(file_path: str, access: OpenAccess = OpenAccess.GENERAL_ACCESS)`: Opens a DSS file located at the provided file path. See [File Access Modes](#file-access-modes).

2. `get(record_path: str)`: Retrieves the record data from the currently opened DSS file of the designated path.

Expand Down Expand Up @@ -150,6 +150,32 @@ This library uses (`hecdss.dll` on Windows and `libhecdss.so` on Unix/Linux). h
```


## File Access Modes

`HecDss` takes an optional `access` argument that controls how the DSS file is opened. These
modes map directly to the `access` argument of `hec_dss_open_ex` in the HEC-DSS C library.

| `OpenAccess` | Value | Description |
| ----------- | ----- | ----------- |
| `GENERAL_ACCESS` | 0 | Read, or read/write when the file allows it. No error when the file does not have write permission. This is the default. |
| `READ_ACCESS` | 1 | Read only. The file must already exist and is never written to, so several processes can read the same file at the same time. |
| `MULTI_USER_ACCESS` | 2 | Read/write with full multi-user access. Usually slow, but necessary when more than one process writes to the file. |
| `SINGLE_USER_ADVISORY_ACCESS` | 3 | Read/write with multi-user advisory access. Errors when the file is read only. |
| `EXCLUSIVE_ACCESS` | 4 | Exclusive write, used for squeezing. Errors when exclusive access is not available. |

```python
from hecdss import OpenAccess, HecDss

# open a file read only; several processes can do this at the same time
with HecDss("example.dss", OpenAccess.READ_ACCESS) as dss:
data = dss.get("/example/data/////")
print(dss.access, dss.readonly) # OpenAccess.READ_ACCESS True
```

A file opened with `READ_ACCESS` must already exist; opening a missing file raises `FileNotFoundError`
instead of creating it. Methods that modify the file (`put`, `delete`, `writePrecompressedGrid`)
raise `PermissionError` on a `READ_ACCESS` file.

## Message Levels

Message levels can be set using `set_global_debug_level` or `set_debug_level`, for example:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hec-dss-python"
version = "0.1.32"
version = "0.1.33"
description = "Python wrapper for the HEC-DSS file database C library."
authors = ["Hydrologic Engineering Center"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = hecdss
version = 0.1.32
version = 0.1.33
author = Hydrologic Engineering Center
author_email =hec.dss@usace.army.mil
description = Python wrapper for the HEC-DSS file database C library.
Expand Down
8 changes: 4 additions & 4 deletions src/hecdss/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

from hecdss.array_container import ArrayContainer
from hecdss.catalog import Catalog
from hecdss.hecdss import HecDss
from hecdss.dss_access import OpenAccess
from hecdss.dsspath import DssPath
from hecdss.hecdss import HecDss
from hecdss.irregular_timeseries import IrregularTimeSeries
from hecdss.regular_timeseries import RegularTimeSeries
from hecdss.array_container import ArrayContainer
from hecdss.paired_data import PairedData
from hecdss.regular_timeseries import RegularTimeSeries
from hecdss.text import Text

11 changes: 6 additions & 5 deletions src/hecdss/download_hecdss.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Helper module to retrieve the binary libraries"""

from pathlib import Path
import os
import shutil
import requests
import zipfile
import os
from pathlib import Path

import requests


def download_and_unzip(url, zip_file, destination_dir):
Expand All @@ -21,7 +22,7 @@ def download_and_unzip(url, zip_file, destination_dir):
"""
print(url)
os.makedirs(destination_dir, exist_ok=True)
response = requests.get(zip_url, timeout=300)
response = requests.get(url, timeout=300)
if response.status_code == 200:
zip_file_path = os.path.join(destination_dir, zip_file)
with open(zip_file_path, "wb") as zip_file:
Expand All @@ -36,7 +37,7 @@ def download_and_unzip(url, zip_file, destination_dir):
print(f"Failed to download zip file. Status code: {response.status_code}")

base_url = "https://www.hec.usace.army.mil/nexus/repository/maven-public/mil/army/usace/hec/hecdss/"
version = "7-JA-8"
version = "7-JA-9"

destination_dir = Path(__file__).parent.joinpath("lib")
zip_url = f"{base_url}{version}-win-x86_64/hecdss-{version}-win-x86_64.zip"
Expand Down
32 changes: 32 additions & 0 deletions src/hecdss/dss_access.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider naming this without dss prefix, because we are inside dss already. perhaps Access , or OpenAccess, or...

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from enum import IntEnum


class OpenAccess(IntEnum):
"""OpenAccess is an enumeration of the ways a DSS file can be opened

The values match the access argument of hec_dss_open_ex in the HEC-DSS C
library.

Returns:
OpenAccess: the access mode
"""

GENERAL_ACCESS = 0
"""Read or read/write, whichever the file allows. No error when the file
does not have write permission. This is the default."""

READ_ACCESS = 1
"""Read only; the file will not be written to, and must already exist.
Several processes may hold the same file open this way at once."""

MULTI_USER_ACCESS = 2
"""Read/write with full multi-user access. Usually slow, but necessary when
more than one process writes to the file at the same time."""

SINGLE_USER_ADVISORY_ACCESS = 3
"""Read/write with multi-user advisory access. Errors when the file is read
only."""

EXCLUSIVE_ACCESS = 4
"""Exclusive write, used for squeezing. Errors when exclusive access is not
available."""
48 changes: 45 additions & 3 deletions src/hecdss/hecdss.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from hecdss.array_container import ArrayContainer
from hecdss.catalog import Catalog
from hecdss.dateconverter import DateConverter
from hecdss.dss_access import OpenAccess
from hecdss.dsspath import DssPath
from hecdss.gridded_data import GriddedData
from hecdss.irregular_timeseries import IrregularTimeSeries
Expand All @@ -27,19 +28,52 @@ class HecDss:
""" Main class for working with DSS files
"""

def __init__(self, filename: str):
def __init__(self, filename: str, access: OpenAccess = OpenAccess.GENERAL_ACCESS):
"""constructor for HecDSS

Args:
filename (str): DSS filename to be opened; it will be created if it doesn't exist.
filename (str): DSS filename to be opened; it will be created if it
doesn't exist, unless access is OpenAccess.READ_ACCESS.
access (OpenAccess): how the file is opened. dss_access.py defines access modes

Raises:
ValueError: access is not one of the OpenAccess values.
FileNotFoundError: access is READ_ACCESS and the file does not exist.
"""

self._access = OpenAccess(access)
self._native = _Native()
self._native.hec_dss_open(filename)
self._native.hec_dss_open(filename, self._access)
self._catalog = None
self._filename = filename
self._closed = False

@property
def access(self) -> OpenAccess:
"""OpenAccess: the access mode this file was opened with"""
return self._access

@property
def readonly(self) -> bool:
"""bool: True when this file was opened with OpenAccess.READ_ACCESS"""
return self._access == OpenAccess.READ_ACCESS

def _assert_writable(self, operation: str) -> None:
"""raises if this file was opened read only

Args:
operation (str): name of the method being attempted, used in the message.

Raises:
PermissionError: the file was opened with OpenAccess.READ_ACCESS.
"""
if self.readonly:
raise PermissionError(
f"cannot {operation}: '{self._filename}' was opened with "
f"{self._access.name}. Reopen it with a read/write access mode "
"to modify it."
)

def __enter__(self):
"""
Enter the runtime context related to this object.
Expand Down Expand Up @@ -609,10 +643,12 @@ def put(self, container) -> int:

Raises:
NotImplementedError: if saving the type of container is not supported.
PermissionError: if the file was opened with OpenAccess.READ_ACCESS.

Returns:
int: status of zero when successful. Non zero for errors.
"""
self._assert_writable("put")
# TODO. is timezone needed?
status = 0
if type(container) is RegularTimeSeries:
Expand Down Expand Up @@ -712,9 +748,12 @@ def writePrecompressedGrid(self, gd, compressedData, CompressionSize):
Args
compressedData (bytes): Compressed data.
CompressionSize (int): Size of the compressed data.
Raises:
PermissionError: if the file was opened with OpenAccess.READ_ACCESS.
Returns:
int: 0 if successful, -1 otherwise.
"""
self._assert_writable("writePrecompressedGrid")

if compressedData and CompressionSize > 0:
status = self._native.hec_dss_gridStore(gd, compressedData, CompressionSize)
Expand All @@ -729,9 +768,12 @@ def delete(self, pathname: str, allrecords: bool = False, startdatetime=None, en
allRecords (bool): if True, delete all records with this pathname that have different dates
startdatetime (datetime): start date for query, if only start date is provided, delete all records at or after this date
enddatetime (datetime): end date for the query, if only end date is provided, delete all records at or before this date
Raises:
PermissionError: if the file was opened with OpenAccess.READ_ACCESS.
Returns:
int: status of zero when successful. Non zero for errors.
"""
self._assert_writable("delete")
rt = self.get_record_type(pathname)
delete_path = DssPath(pathname)
if (rt == RecordType.RegularTimeSeries or rt == RecordType.IrregularTimeSeries) and allrecords:
Expand Down
36 changes: 31 additions & 5 deletions src/hecdss/native.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import numpy as np

from hecdss.dss_access import OpenAccess


# from hecdss.location_info import LocationInfo

Expand Down Expand Up @@ -76,25 +78,49 @@ def __init__(self):
else:
self.dll = self.load_hecdss_library("libhecdss.so")

def hec_dss_open(self, dss_filename: str) -> int:
def hec_dss_open(self, dss_filename: str, access: int = OpenAccess.GENERAL_ACCESS) -> int:
"""opens a DSS file and gets a handle

Args:
dss_filename (str): filename to open
dss_filename (str): filename to open; it is created if it doesn't
exist, except when access is READ_ACCESS.
access (int): read/write access used to open the file, see OpenAccess
0 - GENERAL_ACCESS: Doesn't matter (no error if file doesn't have write permission)
1 - READ_ACCESS: Read only (will not allow writing to file)
2 - MULTI_USER_ACCESS: Read/Write permission with full multi-user access
(usually slow, but necessary for multiple processes)
3 - SINGLE_USER_ADVISORY_ACCESS: Read/Write permission with multi-user advisory
access (throws an error if file is read only)
4 - EXCLUSIVE_ACCESS: Exclusive write (used for squeezing).
Throws an error if not available.

Returns:
int: status of zero when successful, non-zero on error.

Raises:
ValueError: access is not one of the OpenAccess values.
FileNotFoundError: READ_ACCESS was requested and the file is missing.
Exception: the file could not be opened.
"""
f = self.dll.hec_dss_open
access = OpenAccess(access)

if access == OpenAccess.READ_ACCESS and not os.path.exists(dss_filename):
raise FileNotFoundError(
f"DSS file not found: '{dss_filename}'. "
f"A file must already exist to be opened with {access.name}."
)

f = self.dll.hec_dss_open_ex
f.argtypes = [
c_char_p,
POINTER(c_void_p),
c_int,
]
f.restype = c_int
self.handle = c_void_p()
rval = f(dss_filename.encode("utf-8"), ctypes.byref(self.handle))
rval = f(dss_filename.encode("utf-8"), byref(self.handle), c_int(access))
if rval != 0:
raise Exception("Error opening DSS file.")
raise Exception(f"Error opening DSS file. status = {rval}")
return rval

def hec_dss_close(self):
Expand Down
Loading
Loading