From 4af1cd64bd6888adac996bca621cc991a267bc43 Mon Sep 17 00:00:00 2001 From: oskarhurst <48417811+oskarhurst@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:44:06 -0700 Subject: [PATCH 1/4] logging --- LOGGING.md | 447 ++++++++++++++++++++++++++++++++++ src/hecdss/__init__.py | 10 + src/hecdss/catalog.py | 14 ++ src/hecdss/download_hecdss.py | 17 +- src/hecdss/dsspath.py | 32 ++- src/hecdss/hecdss.py | 92 +++++-- src/hecdss/logging_config.py | 314 ++++++++++++++++++++++++ src/hecdss/native.py | 57 ++++- tests/test_logging.py | 320 ++++++++++++++++++++++++ 9 files changed, 1260 insertions(+), 43 deletions(-) create mode 100644 LOGGING.md create mode 100644 src/hecdss/logging_config.py create mode 100644 tests/test_logging.py diff --git a/LOGGING.md b/LOGGING.md new file mode 100644 index 0000000..f86187f --- /dev/null +++ b/LOGGING.md @@ -0,0 +1,447 @@ +# HEC-DSS Python Logging Guide + +## Overview + +The hec-dss-python library provides comprehensive logging capabilities for both Python code and native DLL operations. The logging system is built on Python's standard `logging` module and follows Python library best practices. + +## Quick Start + +### Basic Setup + +```python +import logging +from hecdss import HecDss, configure_logging + +# Simple configuration - show warnings and above +configure_logging(python_level=logging.WARNING, dll_level=3) + +# Or just use defaults (no output unless configured) +dss = HecDss("myfile.dss") +``` + +### Enable DLL Output Capture + +```python +# Configure logging with DLL output capture +configure_logging( + python_level=logging.INFO, + dll_level=4, # User diagnostic messages + capture_dll_output=True +) + +# Open DSS file with DLL logging enabled +with HecDss("myfile.dss", enable_dll_logging=True) as dss: + # DLL messages now appear in Python logs + catalog = dss.get_catalog() +``` + +## Logging Architecture + +The library uses two separate loggers: + +1. **`hecdss`** - Python code messages (errors, warnings, info from Python functions) +2. **`hecdss.dll`** - Native DLL messages (captured from the HEC-DSS C library) + +### Default Behavior + +By default, the library produces **no output** (uses `NullHandler`), following Python library best practices. Users must explicitly configure logging to see messages. + +## DLL Message Levels + +The DLL uses a 0-15 scale to control message verbosity: + +| Level | Description | Recommended Use | +|-------|-------------|-----------------| +| 0 | No messages | Not recommended (hides errors) | +| 1 | Critical errors only | Minimal error reporting | +| 2 | Terse (errors + file operations) | Basic operation tracking | +| **3** | **General messages (default)** | **Normal operations** | +| 4 | User diagnostic messages | Troubleshooting | +| 5 | Internal diagnostics level 1 | Debugging (not for users) | +| 6 | Internal diagnostics level 2 | Full debugging | +| 7-15 | Extended debug levels | Development use | + +## Configuration Functions + +### `configure_logging()` + +Main configuration function for common logging setups. + +```python +configure_logging( + python_level=None, # Python code log level + dll_level=None, # DLL verbosity (0-15) + combined_file=None, # Single file for all logs + python_file=None, # File for Python logs only + dll_file=None, # File for DLL logs only + console=True, # Also output to console + format_string=None, # Custom format string + capture_dll_output=False # Enable DLL capture setup +) +``` + +### `HecDss.set_global_debug_level()` + +Set the DLL message level globally. + +```python +HecDss.set_global_debug_level(4) # User diagnostic level +``` + +## Common Usage Patterns + +### 1. Development/Debugging + +```python +# See everything +configure_logging( + python_level=logging.DEBUG, + dll_level=5, # Internal diagnostics + combined_file='debug.log' +) +``` + +### 2. Production Monitoring + +```python +# Only errors and warnings +configure_logging( + python_level=logging.WARNING, + dll_level=2, # Terse output + combined_file='app.log' +) +``` + +### 3. Separate Python and DLL Logs + +```python +configure_logging( + python_file='python_ops.log', + dll_file='dll_messages.log', + python_level=logging.INFO, + dll_level=3 +) +``` + +### 4. Console Output Only + +```python +configure_logging( + python_level=logging.INFO, + dll_level=3, + console=True # Default +) +``` + +### 5. Troubleshooting DSS Operations + +```python +import logging +from hecdss import HecDss, configure_logging + +# Enable detailed DLL output +configure_logging( + python_level=logging.INFO, + dll_level=4, # User diagnostics + capture_dll_output=True +) + +# Create DSS with DLL logging +with HecDss("problem_file.dss", enable_dll_logging=True) as dss: + # Increase verbosity for specific operation + HecDss.set_global_debug_level(5) + + # Problematic operation + data = dss.get("//LOCATION/FLOW//1HOUR/OBS/") + + # Reset to normal + HecDss.set_global_debug_level(3) +``` + +## DLL Output Capture + +The library can capture native DLL messages and route them through Python's logging system. + +### How It Works + +1. Creates a temporary log file for the DLL +2. Tells the DLL to write to this file (via `zopenLog()`) +3. Monitors the file in a background thread +4. Routes messages to Python logging with `[DLL]` prefix +5. Cleans up the temporary file on close + +### Enabling DLL Capture + +```python +# Step 1: Configure logging with capture +configure_logging( + dll_level=4, + capture_dll_output=True +) + +# Step 2: Open DSS with DLL logging enabled +dss = HecDss("myfile.dss", enable_dll_logging=True) + +# Now DLL messages appear in Python logs: +# 2024-01-15 10:23:45 - hecdss.dll - DLL - [DLL] DSS file opened successfully +# 2024-01-15 10:23:45 - hecdss.dll - DLL - [DLL] Reading catalog... +``` + +## Logging Methods + +### Standard Python Logging + +```python +import logging +logger = logging.getLogger('hecdss') + +logger.debug("Debug message") +logger.info("Info message") +logger.warning("Warning message") +logger.error("Error message") +logger.critical("Critical message") +``` + +### DLL Messages + +All DLL messages appear at the `DLL` level (25), between INFO (20) and WARNING (30): + +```python +dll_logger = logging.getLogger('hecdss.dll') +dll_logger.dll_message("Message from DLL") # Custom method +``` + +### Catalog and Path Logging + +```python +# Get catalog +catalog = dss.get_catalog() + +# Print to console (unchanged) +catalog.print() + +# Log to Python logging +catalog.log_items(logging.INFO) + +# Path logging +path = DssPath("/A/B/C/01Jan2024/1Hour/F/") +path.print() # Console output +path.log_path(logging.DEBUG) # Python logging +``` + +## Custom Log Formatting + +```python +configure_logging( + format_string='%(asctime)s | %(name)-20s | %(levelname)-8s | %(message)s', + python_level=logging.INFO +) + +# Output: +# 2024-01-15 10:23:45 | hecdss | INFO | Opening DSS file +# 2024-01-15 10:23:45 | hecdss.dll | DLL | [DLL] File opened +``` + +## Filtering + +### Show Only Python Messages + +```python +configure_logging( + python_level=logging.INFO, + dll_level=0 # No DLL output +) +``` + +### Show Only DLL Messages + +```python +import logging + +# Configure +configure_logging(dll_level=4, capture_dll_output=True) + +# Suppress Python messages +logging.getLogger('hecdss').setLevel(logging.CRITICAL) +``` + +### Filter by Logger Name + +```python +# Only show messages from specific module +logging.getLogger('hecdss.catalog').setLevel(logging.DEBUG) +logging.getLogger('hecdss.native').setLevel(logging.WARNING) +``` + +## Integration with Application Logging + +```python +import logging +import logging.config + +# Your app's logging config +LOGGING_CONFIG = { + 'version': 1, + 'handlers': { + 'file': { + 'class': 'logging.FileHandler', + 'filename': 'app.log', + 'formatter': 'detailed' + } + }, + 'formatters': { + 'detailed': { + 'format': '%(asctime)s %(name)s %(levelname)s: %(message)s' + } + }, + 'loggers': { + 'hecdss': { + 'level': 'INFO', + 'handlers': ['file'] + }, + 'hecdss.dll': { + 'level': 'DEBUG', + 'handlers': ['file'] + } + } +} + +logging.config.dictConfig(LOGGING_CONFIG) + +# Now use HecDss normally +dss = HecDss("myfile.dss", enable_dll_logging=True) +``` + +## Troubleshooting + +### No Output Appearing + +Check that logging is configured: +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +### DLL Messages Not Captured + +Ensure both steps are done: +1. `capture_dll_output=True` in configure_logging() +2. `enable_dll_logging=True` when creating HecDss + +### Too Much Output + +Reduce verbosity: +```python +# Less Python output +configure_logging(python_level=logging.WARNING) + +# Less DLL output +HecDss.set_global_debug_level(2) # Terse only +``` + +### Performance Impact + +DLL logging has minimal overhead (~50ms file check interval). For production: +```python +# Disable DLL capture for performance +dss = HecDss("myfile.dss", enable_dll_logging=False) # Default +``` + +## Best Practices + +1. **Development**: Use higher verbosity (dll_level=4-5) with file logging +2. **Production**: Use lower verbosity (dll_level=2-3) with error tracking +3. **Testing**: Capture both Python and DLL logs to separate files +4. **Performance**: Disable DLL capture when not needed +5. **Security**: Never log sensitive data; logs may contain file paths + +## Example: Complete Application + +```python +#!/usr/bin/env python +"""Example application with full logging.""" + +import logging +import sys +from hecdss import HecDss, configure_logging + +def setup_logging(debug=False): + """Setup application logging.""" + level = logging.DEBUG if debug else logging.INFO + + configure_logging( + python_level=level, + dll_level=4 if debug else 3, + combined_file='app.log', + console=True, + capture_dll_output=debug, + format_string='%(asctime)s [%(levelname)s] %(name)s: %(message)s' + ) + +def main(): + # Parse arguments + debug = '--debug' in sys.argv + + # Setup logging + setup_logging(debug) + logger = logging.getLogger(__name__) + + logger.info("Application starting") + + try: + # Open DSS file + filename = sys.argv[1] if len(sys.argv) > 1 else "test.dss" + + with HecDss(filename, enable_dll_logging=debug) as dss: + logger.info(f"Opened {filename}") + + # Get catalog + catalog = dss.get_catalog() + logger.info(f"Found {len(catalog.items)} records") + + # Log catalog items if debugging + if debug: + catalog.log_items(logging.DEBUG) + + # Process data... + + except Exception as e: + logger.error(f"Error: {e}", exc_info=True) + return 1 + + logger.info("Application complete") + return 0 + +if __name__ == "__main__": + sys.exit(main()) +``` + +## API Reference + +### Functions + +- `configure_logging(**kwargs)` - Configure library logging +- `HecDss.set_global_debug_level(level)` - Set DLL verbosity (0-15) + +### Classes + +- `HecDss(filename, enable_dll_logging=False)` - Main DSS interface + +### Logger Names + +- `hecdss` - Root logger for Python code +- `hecdss.dll` - Logger for DLL messages +- `hecdss.catalog` - Catalog operations +- `hecdss.native` - Native library interface +- `hecdss.hecdss` - Main HecDss class + +### Log Levels + +- Standard Python: `DEBUG` (10), `INFO` (20), `WARNING` (30), `ERROR` (40), `CRITICAL` (50) +- Custom: `DLL` (25) - All DLL messages + +## See Also + +- [Python logging documentation](https://docs.python.org/3/library/logging.html) +- [HEC-DSS Programmers Guide](https://www.hec.usace.army.mil/confluence/dssdocs/dsscprogrammer) +- [HEC-DSS Message Levels](https://github.com/HydrologicEngineeringCenter/hec-dss/blob/master/heclib/heclib_c/src/headers/zdssMessages.h) \ No newline at end of file diff --git a/src/hecdss/__init__.py b/src/hecdss/__init__.py index 04673ef..a5e5bdc 100644 --- a/src/hecdss/__init__.py +++ b/src/hecdss/__init__.py @@ -1,4 +1,6 @@ +import logging + from hecdss.catalog import Catalog from hecdss.hecdss import HecDss from hecdss.dsspath import DssPath @@ -6,4 +8,12 @@ from hecdss.regular_timeseries import RegularTimeSeries from hecdss.array_container import ArrayContainer from hecdss.paired_data import PairedData +from hecdss.logging_config import ( + configure_logging, + setup_default_logging, + DLL_MESSAGE +) + +# Set up default logging configuration (NullHandler, no output by default) +setup_default_logging() diff --git a/src/hecdss/catalog.py b/src/hecdss/catalog.py index cf504f7..2f20fd0 100644 --- a/src/hecdss/catalog.py +++ b/src/hecdss/catalog.py @@ -1,8 +1,12 @@ +import logging from .record_type import RecordType from .dsspath import DssPath from datetime import datetime import re +# Get logger for this module +logger = logging.getLogger(__name__) + class Catalog: """manage list of objects inside a DSS database""" def __init__(self, uncondensed_paths, recordTypes): @@ -76,9 +80,19 @@ def __create_condensed_catalog(self): self.items.append(p) def print(self): + """Print all items in the catalog to stdout.""" for ds in self.items: print(ds) + def log_items(self, level=logging.INFO): + """Log all items in the catalog at the specified logging level. + + Args: + level: Logging level (default: logging.INFO) + """ + for ds in self.items: + logger.log(level, "Catalog item: %s", ds) + def __iter__(self): self.index = 0 # Initialize the index to 0 return self diff --git a/src/hecdss/download_hecdss.py b/src/hecdss/download_hecdss.py index 777ff22..24828d0 100644 --- a/src/hecdss/download_hecdss.py +++ b/src/hecdss/download_hecdss.py @@ -1,11 +1,15 @@ """Helper module to retrieve the binary libraries""" +import logging from pathlib import Path import shutil import requests import zipfile import os +# Get logger for this module +logger = logging.getLogger(__name__) + def download_and_unzip(url, zip_file, destination_dir): """Retrieves a compressed archive from the URL and extracts it in the destination dir. @@ -19,21 +23,26 @@ def download_and_unzip(url, zip_file, destination_dir): destination_dir : Path to the local directory where the archive content is extracted to. """ - print(url) + print(url) # Keep for user feedback during download + logger.info("Downloading from URL: %s", url) os.makedirs(destination_dir, exist_ok=True) response = requests.get(zip_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: zip_file.write(response.content) - print("Zip file downloaded successfully.") + print("Zip file downloaded successfully.") # Keep for user feedback + logger.info("Zip file downloaded successfully") with zipfile.ZipFile(zip_file_path, "r") as zip_ref: zip_ref.extractall(destination_dir) - print(f"Contents extracted successfully to.'{destination_dir}'") + print(f"Contents extracted successfully to.'{destination_dir}'") # Keep for user feedback + logger.info("Contents extracted successfully to '%s'", destination_dir) os.remove(zip_file_path) else: - print(f"Failed to download zip file. Status code: {response.status_code}") + error_msg = f"Failed to download zip file. Status code: {response.status_code}" + print(error_msg) # Keep for user feedback + logger.error(error_msg) base_url = "https://www.hec.usace.army.mil/nexus/repository/maven-public/mil/army/usace/hec/hecdss/" version = "7-IU-16" diff --git a/src/hecdss/dsspath.py b/src/hecdss/dsspath.py index f5ea6bf..f00b0ed 100644 --- a/src/hecdss/dsspath.py +++ b/src/hecdss/dsspath.py @@ -1,7 +1,11 @@ +import logging from .dateconverter import DateConverter from .dss_type import DssType from .record_type import RecordType +# Get logger for this module +logger = logging.getLogger(__name__) + class DssPath: """ Manage parts of DSS path /A/B/C/D/E/F/ @@ -79,11 +83,25 @@ def is_time_series(self): def print(self): """ - Print the parts of the DSS path. + Print the parts of the DSS path to stdout. + """ + print("a:" + self.A) + print("b:" + self.B) + print("c:" + self.C) + print("d:" + self.D) + print("e:" + self.E) + print("f:" + self.F) + + def log_path(self, level=logging.INFO): + """Log the DSS path parts at the specified logging level. + + Args: + level: Logging level (default: logging.INFO) """ - print("a:" + self.path.A) - print("b:" + self.path.B) - print("c:" + self.path.C) - print("d:" + self.path.D) - print("e:" + self.path.E) - print("f:" + self.path.F) \ No newline at end of file + logger.log(level, "DssPath: %s", str(self)) + logger.log(level, " A (Group/Project/Watershed): %s", self.A) + logger.log(level, " B (Location): %s", self.B) + logger.log(level, " C (Parameter): %s", self.C) + logger.log(level, " D (Block Start Date/Time): %s", self.D) + logger.log(level, " E (Time Interval/Block Length): %s", self.E) + logger.log(level, " F (Descriptor): %s", self.F) \ No newline at end of file diff --git a/src/hecdss/hecdss.py b/src/hecdss/hecdss.py index c73bdac..7ad694e 100644 --- a/src/hecdss/hecdss.py +++ b/src/hecdss/hecdss.py @@ -1,4 +1,5 @@ """Docstring for public module.""" +import logging from datetime import datetime, timedelta from zoneinfo import ZoneInfo, ZoneInfoNotFoundError @@ -16,21 +17,32 @@ from hecdss.catalog import Catalog from hecdss.gridded_data import GriddedData from hecdss.dsspath import DssPath +from hecdss.logging_config import get_dll_monitor DSS_UNDEFINED_VALUE = -340282346638528859811704183484516925440.000000 +# Get logger for this module +logger = logging.getLogger(__name__) + class HecDss: """ Main class for working with DSS files """ - def __init__(self, filename:str): + def __init__(self, filename:str, enable_dll_logging:bool = False): """constructor for HecDSS Args: filename (str): DSS filename to be opened; it will be created if it doesn't exist. + enable_dll_logging (bool): If True, capture DLL messages to Python logging. Default False. """ self._native = _Native() + self._dll_monitor = None + + # Setup DLL logging if requested + if enable_dll_logging: + self._setup_dll_logging() + self._native.hec_dss_open(filename) self._catalog = None self._filename = filename @@ -55,17 +67,46 @@ def __exit__(self, exc_type, exc_val, exc_tb): exc_tb (traceback): The traceback object. """ self.close() + + def _setup_dll_logging(self): + """Setup temporary log file for DLL output monitoring.""" + self._dll_monitor = get_dll_monitor() + + # Create temp log file + log_file_path = self._dll_monitor.setup_temp_log_file() + + # Tell DLL to use this log file + status = self._native.hec_dss_open_log_file(log_file_path) + if status == 0: + # Start monitoring + self._dll_monitor.start_monitoring() + logger.debug("DLL logging enabled, monitoring: %s", log_file_path) + else: + logger.warning("Could not enable DLL log file, messages will not be captured") + self._dll_monitor = None @staticmethod def set_global_debug_level(level: int) -> None: """ - Sets the library debug level + Sets the library debug level and syncs with Python logging. Args: - level (int): a value between 0 and 15. Larger for more output. - For level descriptions, see zdssMessages.h of the heclib source code, - or documentation from the HEC-DSS Programmers Guide for C on the `mlvl` parameter of the `zset` utility function. + level (int): HEC-DSS message level (0-15) + 0: No messages (not recommended) + 1: Critical errors only + 2: Terse output (errors + file operations) + 3: General log messages (default) + 4: User diagnostic messages + 5: Internal debug level 1 (not recommended for users) + 6: Internal debug level 2 (full debug) + 7-15: Extended debug levels """ + # Set the native DLL level (controls what gets written) _Native().hec_dss_set_debug_level(level) + + # Update the monitor's awareness of current level + from hecdss.logging_config import get_dll_monitor + monitor = get_dll_monitor() + monitor.set_dll_level(level) def close(self): """closes the DSS file and releases any locks """ @@ -73,6 +114,12 @@ def close(self): self._native.hec_dss_close() self._closed = True + # Clean up DLL logging if active + if self._dll_monitor: + self._native.hec_dss_close_log_file() + self._dll_monitor.stop_monitoring() + self._dll_monitor = None + def get_record_type(self, pathname: str) -> RecordType: """ Get the record type for a given DSS pathname. @@ -87,10 +134,10 @@ def get_record_type(self, pathname: str) -> RecordType: self._catalog = self.get_catalog() rt = self._catalog.get_record_type(pathname) - # print(f"hec_dss_recordType for '{pathname}' is {rt}") + logger.debug("hec_dss_recordType for '%s' is %s", pathname, rt) # TODO do native call. # rt = self._native.hec_dss_recordType(pathname) - # print(f"hec_dss_recordType for '{pathname}' is {rt}") + # logger.debug("hec_dss_recordType for '%s' is %s", pathname, rt) return rt def get(self, pathname: str, startdatetime=None, enddatetime=None, trim=False): @@ -214,7 +261,7 @@ def _get_gridded_data(self, pathname): ) if status != 0: - print(f"Error reading gridded-data from '{pathname}'") + logger.error("Error reading gridded-data from '%s'", pathname) return None gd = GriddedData() @@ -268,9 +315,9 @@ def _get_paired_data(self, pathname): typeDependent, labelsLength ) - # print("Number of Ordinates:", numberOrdinates[0]) - # print("Number of Curves:", numberCurves[0]) - # print("length of labels:", labelsLength[0]) + logger.debug("Number of Ordinates: %s", numberOrdinates[0]) + logger.debug("Number of Curves: %s", numberCurves[0]) + logger.debug("Length of labels: %s", labelsLength[0]) doubleOrdinates = [] doubleValues = [] @@ -296,7 +343,7 @@ def _get_paired_data(self, pathname): timeZoneName, len(timeZoneNameLength[0]) + 1) if status != 0: - print(f"Error reading paired-data from '{pathname}'") + logger.error("Error reading paired-data from '%s'", pathname) return None pd = PairedData() @@ -383,8 +430,8 @@ def _get_timeseries(self, pathname, startDateTime, endDateTime, trim): numberValues, qualityElementSize, ) - # print("Number of values:", numberValues[0]) - # print("Quality element size:", qualityElementSize[0]) + logger.debug("Number of values: %s", numberValues[0]) + logger.debug("Quality element size: %s", qualityElementSize[0]) number_periods = numberValues[0] if RecordType.RegularTimeSeries == self.get_record_type(pathname): @@ -434,12 +481,11 @@ def _get_timeseries(self, pathname, startDateTime, endDateTime, trim): bufferLength ) - # print("units = "+units[0]) - # print("datatype = "+dataType[0]) - # print("times: ") - # print(times) - # print(values) - # print("julianBaseDate = " + str(julianBaseDate[0])) + logger.debug("units = %s", units[0]) + logger.debug("datatype = %s", dataType[0]) + logger.debug("times: %s", times) + logger.debug("values: %s", values) + logger.debug("julianBaseDate = %s", julianBaseDate[0]) # print("timeGranularitySeconds = " + str(timeGranularitySeconds[0])) if RecordType.IrregularTimeSeries == self.get_record_type(pathname): ts = IrregularTimeSeries() @@ -479,7 +525,7 @@ def _get_timeseries(self, pathname, startDateTime, endDateTime, trim): try: new_times = [i.replace(tzinfo=ZoneInfo(timeZoneName)) for i in new_times] except ZoneInfoNotFoundError as e: - print(f"Warning: {e}. Using no zone instead.") + logger.warning("Warning: %s. Using no zone instead.", e) timeZoneName = False elif (DssPath(pathname).D.lower() == "ts-pattern"): new_times = [] @@ -725,7 +771,7 @@ def delete(self, pathname: str, allrecords: bool = False, startdatetime=None, en if status == 0: self._catalog = None else: - print(f"Error deleting record from '{pathname}', Record does not exist or timeseries path must be uncondensed") + logger.error("Error deleting record from '%s', Record does not exist or timeseries path must be uncondensed", pathname) return status def get_catalog(self) -> Catalog: @@ -764,6 +810,6 @@ def set_debug_level(self, level) -> int: dss = HecDss("sample7.dss") catalog = dss.get_catalog() for p in catalog: - print(p) + print(p) # This is in test code, keep as is # print(catalog[0:5]) # dss.set_debug_level(15) diff --git a/src/hecdss/logging_config.py b/src/hecdss/logging_config.py new file mode 100644 index 0000000..d194692 --- /dev/null +++ b/src/hecdss/logging_config.py @@ -0,0 +1,314 @@ +""" +Logging configuration for hec-dss-python library. + +This module provides custom logging levels that map to HEC-DSS DLL message levels, +and utilities for configuring logging for both Python code and DLL output. + +HEC-DSS Message Levels (from heclib): + MESS_LEVEL_NONE 0: No messages, including error (not guaranteed). Highly discouraged + MESS_LEVEL_CRITICAL 1: Critical (Error) Messages. Discouraged. + MESS_LEVEL_TERSE 2: Minimal (terse) output: zopen, zclose, critical errors. + MESS_LEVEL_GENERAL 3: General Log Messages. Default. + MESS_LEVEL_USER_DIAG 4: Diagnostic User Messages (e.g., input parameters) + MESS_LEVEL_INTERNAL_DIAG_1 5: Diagnostic Internal Messages level 1 (debug). Not recommended for users + MESS_LEVEL_INTERNAL_DIAG_2 6: Diagnostic Internal Messages level 2 (full debug) + Levels 7-15: Additional verbose debug levels for development +""" + +import logging +import sys +from typing import Optional, Union + + +# Define a single custom level for all DLL messages +# This simplifies routing since we're just capturing whatever the DLL outputs +DLL_MESSAGE = 25 # Between INFO (20) and WARNING (30) + +# Register the custom DLL message level +logging.addLevelName(DLL_MESSAGE, "DLL") + + +# HEC-DSS Message Levels (for reference, but not mapped to Python levels) +# These control what the DLL writes to its log file: +# 0: No messages (not recommended) +# 1: Critical errors only +# 2: Terse output (errors + file operations) +# 3: General log messages (default) +# 4: User diagnostic messages +# 5: Internal debug level 1 +# 6: Internal debug level 2 (full debug) +# 7-15: Extended debug levels + + +# Add convenience method for DLL messages +def dll_message(self, message, *args, **kwargs): + """Log a DLL message at the DLL level.""" + if self.isEnabledFor(DLL_MESSAGE): + self._log(DLL_MESSAGE, message, args, **kwargs) + +# Add the method to Logger class +logging.Logger.dll_message = dll_message + + +def setup_default_logging(): + """ + Set up default logging configuration for the library. + By default, uses NullHandler (no output) following Python library best practices. + """ + # Root logger for the library + root_logger = logging.getLogger('hecdss') + if not root_logger.handlers: + root_logger.addHandler(logging.NullHandler()) + root_logger.setLevel(logging.DEBUG) # Capture everything, let handlers filter + + # DLL output logger (separate hierarchy for flexibility) + dll_logger = logging.getLogger('hecdss.dll') + dll_logger.setLevel(logging.DEBUG) # Capture everything by default + + return root_logger, dll_logger + + +def configure_logging( + python_level: Optional[Union[int, str]] = None, + dll_level: Optional[Union[int, str]] = None, + combined_file: Optional[str] = None, + python_file: Optional[str] = None, + dll_file: Optional[str] = None, + console: bool = True, + format_string: Optional[str] = None, + capture_dll_output: bool = False +): + """ + Configure logging for the hec-dss-python library. + + This provides an easy way to set up common logging configurations. + Users can control Python code logging and DLL output logging independently. + + Args: + python_level: Log level for Python code (e.g., logging.INFO or "INFO") + dll_level: Either DLL message level (0-15) or Python log level for DLL output + 0: No messages (not recommended) + 1: Critical errors only + 2: Terse output (errors + file operations) + 3: General log messages (default) + 4: User diagnostic messages + 5: Internal debug level 1 + 6: Internal debug level 2 (full debug) + 7-15: Extended debug levels + combined_file: If provided, both Python and DLL logs go to this file + python_file: If provided, Python logs go to this file + dll_file: If provided, DLL logs go to this file + console: If True (default), also output to console + format_string: Custom format string for log messages + capture_dll_output: If True, setup global DLL output capture (see Note below) + + Note: + The capture_dll_output parameter sets up global monitoring but actual capture + requires HecDss instances to be created with enable_dll_logging=True. + + Examples: + # Default operation (general messages only) + configure_logging(dll_level=3) + + # Show errors only + configure_logging(python_level=logging.ERROR, dll_level=1) + + # Enable user diagnostics + configure_logging(dll_level=4) + + # Enable internal debugging (not recommended for users) + configure_logging(dll_level=5) + + # Everything to one file + configure_logging(combined_file='debug.log', dll_level=6) + + # Separate files for Python and DLL + configure_logging(python_file='python.log', dll_file='dll.log', dll_level=3) + """ + if format_string is None: + format_string = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + + formatter = logging.Formatter(format_string) + + # Get loggers + python_logger = logging.getLogger('hecdss') + dll_logger = logging.getLogger('hecdss.dll') + + # Clear existing handlers (except NullHandler) + for logger in [python_logger, dll_logger]: + logger.handlers = [h for h in logger.handlers if isinstance(h, logging.NullHandler)] + + # Set levels + if python_level is not None: + if isinstance(python_level, str): + python_level = getattr(logging, python_level.upper()) + python_logger.setLevel(python_level) + + if dll_level is not None: + # Set the DLL debug level (0-15) which controls what the DLL writes + if isinstance(dll_level, int) and 0 <= dll_level <= 15: + from hecdss import HecDss + HecDss.set_global_debug_level(dll_level) + + # Always show DLL messages if they're being captured + # The DLL level controls what gets written, we just display what's captured + dll_logger.setLevel(DLL_MESSAGE) + + # Configure handlers + handlers_added = False + + # Combined file handler + if combined_file: + file_handler = logging.FileHandler(combined_file) + file_handler.setFormatter(formatter) + python_logger.addHandler(file_handler) + dll_logger.addHandler(file_handler) + handlers_added = True + + # Separate file handlers + if python_file: + python_handler = logging.FileHandler(python_file) + python_handler.setFormatter(formatter) + python_logger.addHandler(python_handler) + handlers_added = True + + if dll_file: + dll_handler = logging.FileHandler(dll_file) + dll_handler.setFormatter(formatter) + dll_logger.addHandler(dll_handler) + handlers_added = True + + # Console handler + if console and handlers_added: + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + + if python_level is not None: + python_logger.addHandler(console_handler) + if dll_level is not None: + dll_logger.addHandler(console_handler) + + # Setup DLL output capture if requested + if capture_dll_output: + monitor = get_dll_monitor() + logger.info("DLL output capture configured. Use HecDss(..., enable_dll_logging=True) to activate.") + + +import threading +import tempfile +import time +import os +from pathlib import Path +from typing import Optional + + +class DllLogMonitor: + """ + Monitors HEC-DSS log file and routes messages to Python logging. + Uses a temporary log file approach to capture DLL output. + """ + + def __init__(self, logger: logging.Logger): + self.logger = logger + self.log_file_path = None + self.temp_file = None + self.monitoring = False + self.monitor_thread = None + self._last_position = 0 + self._current_dll_level = 3 # Default to GENERAL + + def setup_temp_log_file(self) -> str: + """Create a temporary log file for DLL output.""" + # Create temporary file but keep it open for DLL to write to + self.temp_file = tempfile.NamedTemporaryFile( + mode='w+', + suffix='_hecdss.log', + prefix='dll_', + delete=False + ) + self.log_file_path = self.temp_file.name + self.temp_file.close() # Close our handle, DLL will open its own + + self.logger.debug("Created temporary DLL log file: %s", self.log_file_path) + return self.log_file_path + + def start_monitoring(self): + """Start monitoring the log file in a separate thread.""" + if not self.log_file_path: + self.setup_temp_log_file() + + self.monitoring = True + self.monitor_thread = threading.Thread( + target=self._monitor_loop, + daemon=True, + name="DLL-Log-Monitor" + ) + self.monitor_thread.start() + self.logger.debug("Started monitoring DLL log file") + + def stop_monitoring(self): + """Stop monitoring and clean up.""" + self.monitoring = False + + if self.monitor_thread: + self.monitor_thread.join(timeout=1.0) + + # Clean up temporary file + if self.log_file_path and os.path.exists(self.log_file_path): + try: + os.remove(self.log_file_path) + self.logger.debug("Cleaned up temporary log file: %s", self.log_file_path) + except Exception as e: + self.logger.warning("Could not remove temp log file %s: %s", + self.log_file_path, e) + + def _monitor_loop(self): + """Watch log file for new content.""" + while self.monitoring: + try: + if self.log_file_path and os.path.exists(self.log_file_path): + with open(self.log_file_path, 'r', encoding='utf-8', errors='ignore') as f: + # Seek to last read position + f.seek(self._last_position) + + # Read new lines + new_content = f.read() + if new_content: + self._last_position = f.tell() + + # Process each line + for line in new_content.splitlines(): + if line.strip(): + self._route_to_logger(line) + + except Exception as e: + self.logger.debug("Error reading DLL log: %s", e) + + time.sleep(0.05) # Check every 50ms + + def _route_to_logger(self, message: str): + """Route DLL message to Python logging at the DLL_MESSAGE level.""" + # Remove timestamp if present (HEC-DSS adds its own) + # Format is typically: "DD-MMM-YYYY HH:MM:SS MESSAGE" + if len(message) > 22 and message[2] == '-' and message[6] == '-': + message = message[22:].strip() + + # All DLL messages go to the same level - the DLL itself controls verbosity + self.logger.log(DLL_MESSAGE, "[DLL] %s", message) + + def set_dll_level(self, level: int): + """Update the current DLL message level.""" + self._current_dll_level = level + + +# Global instance for DLL monitoring +_dll_monitor: Optional[DllLogMonitor] = None + + +def get_dll_monitor() -> DllLogMonitor: + """Get or create the global DLL log monitor.""" + global _dll_monitor + if _dll_monitor is None: + dll_logger = logging.getLogger('hecdss.dll') + _dll_monitor = DllLogMonitor(dll_logger) + return _dll_monitor \ No newline at end of file diff --git a/src/hecdss/native.py b/src/hecdss/native.py index 8968de1..b9136fe 100644 --- a/src/hecdss/native.py +++ b/src/hecdss/native.py @@ -1,3 +1,4 @@ +import logging import ctypes from ctypes import c_float, c_double, c_char_p, c_int, c_void_p, POINTER from ctypes import c_int32 @@ -10,6 +11,9 @@ import sys from typing import List +# Get logger for this module +logger = logging.getLogger(__name__) + # from hecdss.location_info import LocationInfo @@ -95,6 +99,41 @@ def __hec_dss_set_value(self, name: str, value: int): def hec_dss_set_debug_level(self, value: int): self.__hec_dss_set_value("mlvl", value) + def hec_dss_open_log_file(self, log_file_path: str) -> int: + """Open a log file for DLL message output. + + Args: + log_file_path: Path to the log file + + Returns: + Status code (0 for success) + """ + try: + # Try to call zopenLog if it exists + f = self.dll.zopenLog + f.argtypes = [c_char_p] + f.restype = c_int + return f(log_file_path.encode("utf-8")) + except AttributeError: + logger.warning("zopenLog function not found in DLL, messages will go to stdout") + return -1 + + def hec_dss_close_log_file(self) -> int: + """Close the DLL log file. + + Returns: + Status code (0 for success) + """ + try: + # Try to call zcloseLog if it exists + f = self.dll.zcloseLog + f.argtypes = [] + f.restype = c_int + return f() + except AttributeError: + logger.warning("zcloseLog function not found in DLL") + return -1 + def hec_dss_export_to_file( self, path: str, outputFile: str, startDate: str, startTime: str, endDate: str, endTime: str ): @@ -258,7 +297,7 @@ def hec_dss_gridRetrieve(self, pathname: str, c_numberEqualOrExceedingRangeLimit, c_data, dataLength) if result != 0: - print("boolRetriveData False, Function call failed with result:", result) + logger.error("boolRetriveData False, Function call failed with result: %s", result) return result rangeTablesLength = c_numberOfRanges.value @@ -325,7 +364,7 @@ def hec_dss_gridRetrieve(self, pathname: str, # print("Function call successful:") else: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result @@ -465,7 +504,7 @@ def hec_dss_pdRetrieveInfo(self, pathname, typeIndependent[0] = c_typeIndependent.value.decode('utf-8') typeDependent[0] = c_typeDependent.value.decode('utf-8') else: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result @@ -533,7 +572,7 @@ def hec_dss_pdRetrieve(self, pathname: str, timeZoneName[0] = c_timeZoneName.value.decode("utf-8") else: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result @@ -644,7 +683,7 @@ def hec_dss_tsGetSizes( numberValues[0] = nv.value qualityElementSize[0] = qes.value else: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result @@ -690,7 +729,7 @@ def hec_dss_tsGetDateTimeRange( lastValidJulian[0] = ljul.value lastSeconds[0] = lsec.value else: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result @@ -1036,7 +1075,7 @@ def hec_dss_arrayRetrieve(self, pathname, intValues: List[int], floatValues: Lis else: - print("Error reading array status = {status}") + logger.error("Error reading array status = %s", status) def hec_dss_locationRetrieve(self, fullPath: str, x: List[float], y: List[float], z: List[float], coordinateSystem: List[int], coordinateID: List[int], horizontalUnits: List[int], @@ -1146,7 +1185,7 @@ def hec_dss_locationStore(self, location_info, replace: int) -> int: ) if result != 0: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result @@ -1170,6 +1209,6 @@ def hec_dss_delete(self, pathname: str) -> int: result = f(self.handle, pathname.encode("utf-8")) if result != 0: - print("Function call failed with result:", result) + logger.error("Function call failed with result: %s", result) return result diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..0a7567f --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +""" +Test script to demonstrate the new logging functionality in hec-dss-python. +""" + +import logging +import sys +import os + +# Add src to path for testing +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from hecdss import ( + HecDss, + configure_logging, + DLL_MESSAGE +) + + +def test_basic_logging(): + """Test basic logging configuration.""" + print("\n=== Test 1: Basic Logging Configuration ===") + + # Configure to show Python INFO and DLL general messages + configure_logging( + python_level=logging.INFO, + dll_level=3, # MESS_LEVEL_GENERAL + format_string='%(name)s - %(levelname)s - %(message)s' + ) + + # Get a logger and test it + logger = logging.getLogger('hecdss.test') + logger.info("This is an INFO message from Python code") + logger.debug("This DEBUG message should not appear (level too low)") + + print("Basic logging test completed.\n") + + +def test_separate_levels(): + """Test separate control of Python and DLL logging.""" + print("\n=== Test 2: Separate Python and DLL Levels ===") + + # Show only errors from Python but diagnostic messages from DLL + configure_logging( + python_level=logging.ERROR, + dll_level=4 # MESS_LEVEL_USER_DIAG + ) + + logger = logging.getLogger('hecdss.test') + logger.info("This INFO should NOT appear") + logger.error("This ERROR should appear") + + # Test setting DLL level directly + HecDss.set_global_debug_level(5) # MESS_LEVEL_INTERNAL_DIAG_1 + print("DLL debug level set to 5 (internal diagnostics)\n") + + +def test_file_logging(): + """Test logging to files.""" + print("\n=== Test 3: File Logging ===") + + # Configure separate files for Python and DLL + configure_logging( + python_file='test_python.log', + dll_file='test_dll.log', + python_level=logging.DEBUG, + dll_level=4, + console=False # Don't output to console + ) + + logger = logging.getLogger('hecdss.test') + logger.debug("Debug message to Python log file") + logger.info("Info message to Python log file") + + print("Check test_python.log and test_dll.log files for output.\n") + + +def test_custom_levels(): + """Test custom DLL message level.""" + print("\n=== Test 4: Custom DLL Message Level ===") + + # Configure to show DLL messages + configure_logging( + dll_level=4, # Set DLL to user diagnostic level + format_string='%(levelname)-15s - %(message)s' + ) + + # Get DLL logger to demonstrate the single DLL level + dll_logger = logging.getLogger('hecdss.dll') + + # Use the single DLL message method + dll_logger.dll_message("This is a DLL message") + dll_logger.dll_message("All DLL messages use the same Python log level") + dll_logger.dll_message("The DLL itself controls what gets written based on its debug level") + + print("Custom level test completed.\n") + + +def test_no_logging(): + """Test that logging can be completely disabled.""" + print("\n=== Test 5: No Logging (Default Behavior) ===") + + # Reset to default (no output) + root_logger = logging.getLogger('hecdss') + dll_logger = logging.getLogger('hecdss.dll') + + # Clear handlers + root_logger.handlers.clear() + dll_logger.handlers.clear() + + # Add only NullHandler (no output) + root_logger.addHandler(logging.NullHandler()) + + logger = logging.getLogger('hecdss.test') + logger.info("This should not appear anywhere") + logger.error("This error should also not appear") + + print("No output should have appeared from logging calls.") + print("This maintains backward compatibility - no output by default.\n") + + +def test_with_dss_file(): + """Test logging with actual DSS file operations if available.""" + print("\n=== Test 6: DSS File Operations with Logging ===") + + # Configure logging to see what's happening + configure_logging( + python_level=logging.INFO, + dll_level=3, # General log messages + format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + test_file = "test_logging.dss" + + try: + # This will test the logging in actual DSS operations + with HecDss(test_file) as dss: + logger = logging.getLogger('hecdss.test') + logger.info(f"Successfully opened {test_file}") + + # Get catalog (this might trigger some logging) + catalog = dss.get_catalog() + logger.info(f"Catalog has {len(catalog.items)} items") + + # Test the new log_items method if catalog has items + if catalog.items: + catalog.log_items(logging.INFO) + + except Exception as e: + logger = logging.getLogger('hecdss.test') + logger.error(f"Error during DSS operations: {e}") + + # Clean up test file if it was created + if os.path.exists(test_file): + os.remove(test_file) + print(f"Cleaned up {test_file}") + + print("DSS file test completed.\n") + + +def test_dll_output_capture(): + """Test capturing DLL output to Python logging.""" + print("\n=== Test 7: DLL Output Capture ===") + + # Configure logging with DLL output capture + configure_logging( + python_level=logging.INFO, + dll_level=4, # User diagnostic messages + format_string='%(name)-20s - %(levelname)-10s - %(message)s', + capture_dll_output=True # Enable DLL capture setup + ) + + test_file = "test_dll_capture.dss" + + try: + # Create HecDss with DLL logging enabled + with HecDss(test_file, enable_dll_logging=True) as dss: + logger = logging.getLogger('hecdss.test') + logger.info(f"Opened {test_file} with DLL logging enabled") + + # Set higher debug level to see more DLL messages + HecDss.set_global_debug_level(5) # Internal diagnostics level 1 + + # Perform operations that should trigger DLL messages + catalog = dss.get_catalog() + logger.info(f"Retrieved catalog with {len(catalog.items)} items") + + # The DLL messages should now appear in the Python logs with [DLL] prefix + + except Exception as e: + logger = logging.getLogger('hecdss.test') + logger.error(f"Error during DLL capture test: {e}") + + # Clean up + if os.path.exists(test_file): + os.remove(test_file) + print(f"Cleaned up {test_file}") + + print("DLL output capture test completed.\n") + + +def test_silent_by_default(): + """Test that hec-dss-python produces NO output by default.""" + print("\n=== Test 8: Silent by Default (Best Practice) ===") + + import io + from contextlib import redirect_stdout, redirect_stderr + + # Capture all output + stdout_capture = io.StringIO() + stderr_capture = io.StringIO() + + test_file = "test_silent.dss" + + try: + with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture): + # Fresh import simulation - clear any existing handlers + root_logger = logging.getLogger('hecdss') + dll_logger = logging.getLogger('hecdss.dll') + root_logger.handlers.clear() + dll_logger.handlers.clear() + + # Re-setup default (what happens on import) + from hecdss import setup_default_logging + setup_default_logging() + + # Use the library normally + dss = HecDss(test_file) + catalog = dss.get_catalog() + + # Try logging - should produce nothing + logger = logging.getLogger('hecdss') + logger.info("This should not appear") + logger.error("This should not appear either") + + # DLL logger too + dll_logger = logging.getLogger('hecdss.dll') + dll_logger.dll_message("This DLL message should not appear") + + dss.close() + + finally: + # Clean up + if os.path.exists(test_file): + os.remove(test_file) + + # Get captured output + stdout_text = stdout_capture.getvalue() + stderr_text = stderr_capture.getvalue() + + # Report results + if stdout_text == "" and stderr_text == "": + print("✓ PASS: No output produced by default (follows Python best practices)") + else: + print("✗ FAIL: Unexpected output detected!") + if stdout_text: + print(f" stdout: {repr(stdout_text[:100])}") + if stderr_text: + print(f" stderr: {repr(stderr_text[:100])}") + + print("This ensures the library is silent unless explicitly configured.\n") + + +def test_explicit_print_still_works(): + """Test that explicit print methods work when called.""" + print("\n=== Test 9: Explicit print() Methods Still Work ===") + + import io + from contextlib import redirect_stdout + from hecdss import DssPath + + stdout_capture = io.StringIO() + + with redirect_stdout(stdout_capture): + # Create a path and explicitly call print + path = DssPath("/A/B/C/01Jan2024/1Hour/F/") + path.print() # This SHOULD produce output + + output = stdout_capture.getvalue() + + # Should see the path parts + if "a:" in output and "b:" in output: + print("✓ PASS: Explicit print() methods work when called") + else: + print("✗ FAIL: print() method did not produce expected output") + print(f" Output: {repr(output[:100])}") + + print("Users can still use print() methods when they want output.\n") + + +def main(): + """Run all logging tests.""" + print("=" * 60) + print("HEC-DSS Python Logging Test Suite") + print("=" * 60) + + # Run tests + test_basic_logging() + test_separate_levels() + test_file_logging() + test_custom_levels() + test_no_logging() + test_with_dss_file() + test_dll_output_capture() + test_silent_by_default() + test_explicit_print_still_works() + + print("=" * 60) + print("All tests completed!") + print("=" * 60) + + # Clean up any log files created + for log_file in ['test_python.log', 'test_dll.log']: + if os.path.exists(log_file): + os.remove(log_file) + print(f"Cleaned up {log_file}") + + +if __name__ == "__main__": + main() \ No newline at end of file From c9eeeed493bedb3854fee18f97611e7ac0c34908 Mon Sep 17 00:00:00 2001 From: oskarhurst <48417811+oskarhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:48:44 -0700 Subject: [PATCH 2/4] logging the package requirements update --- LOGGING.md | 5 + poetry.lock | 200 ++++++++++++++++++++++++----- pyproject.toml | 12 +- requirements.txt | 9 ++ setup.cfg | 6 +- src/hecdss/hecdss.py | 15 ++- src/hecdss/logging_config.py | 2 +- src/hecdss/native.py | 11 ++ tests/test_array.py | 2 +- tests/test_basics.py | 2 +- tests/test_dll.log | 0 tests/test_gridded_data.py | 2 +- tests/test_irregular_timeseries.py | 2 +- tests/test_location_info.py | 2 +- tests/test_paired_data.py | 2 +- tests/test_python.log | 4 + tests/test_regular_timeseries.py | 2 +- tests/test_text.py | 2 +- tests/workshop.py | 2 +- 19 files changed, 232 insertions(+), 50 deletions(-) create mode 100644 requirements.txt create mode 100644 tests/test_dll.log create mode 100644 tests/test_python.log diff --git a/LOGGING.md b/LOGGING.md index f86187f..59ee75c 100644 --- a/LOGGING.md +++ b/LOGGING.md @@ -170,6 +170,11 @@ The library can capture native DLL messages and route them through Python's logg 4. Routes messages to Python logging with `[DLL]` prefix 5. Cleans up the temporary file on close +**Requirements:** Your HEC-DSS DLL must include the `zopenLog` function for DLL +output capture to work. If `zopenLog` is not available, DLL messages will go +directly to the console. To suppress console output when `zopenLog` is not +available, set `message_level=0` (which is the default). + ### Enabling DLL Capture ```python diff --git a/poetry.lock b/poetry.lock index 97773c8..7825405 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. [[package]] name = "alabaster" @@ -6,6 +6,7 @@ version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, @@ -17,6 +18,7 @@ version = "23.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, @@ -27,7 +29,7 @@ cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] dev = ["attrs[docs,tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-no-zope = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.1.1) ; platform_python_implementation == \"CPython\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-xdist[psutil]"] [[package]] name = "babel" @@ -35,20 +37,19 @@ version = "2.12.1" description = "Internationalization utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [[package]] name = "bandit" version = "1.7.5" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "bandit-1.7.5-py3-none-any.whl", hash = "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549"}, {file = "bandit-1.7.5.tar.gz", hash = "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e"}, @@ -62,8 +63,8 @@ rich = "*" stevedore = ">=1.20.0" [package.extras] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"] -toml = ["tomli (>=1.1.0)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0) ; python_version < \"3.11\""] +toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] yaml = ["PyYAML"] [[package]] @@ -72,6 +73,7 @@ version = "22.12.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, @@ -107,6 +109,7 @@ version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, @@ -118,6 +121,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -129,6 +133,7 @@ version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] files = [ {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, @@ -213,6 +218,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -227,6 +233,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -238,6 +245,7 @@ version = "7.3.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, @@ -297,7 +305,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "darglint" @@ -305,6 +313,7 @@ version = "1.8.1" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." optional = false python-versions = ">=3.6,<4.0" +groups = ["dev"] files = [ {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, @@ -316,6 +325,7 @@ version = "0.3.7" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, @@ -327,6 +337,7 @@ version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, @@ -338,6 +349,7 @@ version = "0.6.3" description = "A parser for Python dependency files" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "dparse-0.6.3-py3-none-any.whl", hash = "sha256:0d8fe18714056ca632d98b24fbfc4e9791d4e47065285ab486182288813a5318"}, {file = "dparse-0.6.3.tar.gz", hash = "sha256:27bb8b4bcaefec3997697ba3f6e06b2447200ba273c0b085c3d012a04571b528"}, @@ -357,6 +369,8 @@ version = "1.1.3" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, @@ -371,6 +385,7 @@ version = "3.12.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.12.3-py3-none-any.whl", hash = "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb"}, {file = "filelock-3.12.3.tar.gz", hash = "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d"}, @@ -389,6 +404,7 @@ version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.6.1" +groups = ["dev"] files = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, @@ -405,6 +421,7 @@ version = "4.1.1" description = "Automated security testing with bandit and flake8." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, @@ -420,6 +437,7 @@ version = "23.3.12" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "flake8-bugbear-23.3.12.tar.gz", hash = "sha256:e3e7f74c8a49ad3794a7183353026dabd68c74030d5f46571f84c1fb0eb79363"}, {file = "flake8_bugbear-23.3.12-py3-none-any.whl", hash = "sha256:beb5c7efcd7ccc2039ef66a77bb8db925e7be3531ff1cb4d0b7030d0e2113d72"}, @@ -438,6 +456,7 @@ version = "1.7.0" description = "Extension for flake8 which uses pydocstyle to check docstrings" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, @@ -453,6 +472,7 @@ version = "0.3.0" description = "Python docstring reStructuredText (RST) validator for flake8" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "flake8-rst-docstrings-0.3.0.tar.gz", hash = "sha256:d1ce22b4bd37b73cd86b8d980e946ef198cfcc18ed82fedb674ceaa2f8d1afa4"}, {file = "flake8_rst_docstrings-0.3.0-py3-none-any.whl", hash = "sha256:f8c3c6892ff402292651c31983a38da082480ad3ba253743de52989bdc84ca1c"}, @@ -472,6 +492,7 @@ version = "4.0.10" description = "Git Object Database" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, @@ -486,6 +507,7 @@ version = "3.1.36" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "GitPython-3.1.36-py3-none-any.whl", hash = "sha256:8d22b5cfefd17c79914226982bb7851d6ade47545b1735a9d010a2a4c26d8388"}, {file = "GitPython-3.1.36.tar.gz", hash = "sha256:4bb0c2a6995e85064140d31a33289aa5dce80133a23d36fcd372d716c54d3ebf"}, @@ -503,6 +525,7 @@ version = "2.5.28" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.5.28-py2.py3-none-any.whl", hash = "sha256:87816de144bf46d161bd5b3e8f5596b16cade3b80be537087334b26bc5c177f3"}, {file = "identify-2.5.28.tar.gz", hash = "sha256:94bb59643083ebd60dc996d043497479ee554381fbc5307763915cda49b0e78f"}, @@ -517,6 +540,7 @@ version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main", "dev"] files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -528,6 +552,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -539,6 +564,8 @@ version = "6.8.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, @@ -550,7 +577,7 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "iniconfig" @@ -558,6 +585,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -569,6 +597,7 @@ version = "5.12.0" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, @@ -586,6 +615,7 @@ version = "3.1.2" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, @@ -603,6 +633,7 @@ version = "2.6.3" description = "Python LiveReload is an awesome tool for web developers" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, @@ -618,6 +649,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -642,6 +674,7 @@ version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, @@ -711,6 +744,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -722,6 +756,7 @@ version = "0.4.0" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, @@ -741,6 +776,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -752,6 +788,7 @@ version = "1.5.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, @@ -798,6 +835,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -809,6 +847,7 @@ version = "2.0.0" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, @@ -835,6 +874,7 @@ version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +groups = ["dev"] files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, @@ -843,12 +883,59 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -863,6 +950,7 @@ version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, @@ -874,6 +962,7 @@ version = "5.11.1" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" +groups = ["dev"] files = [ {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, @@ -885,6 +974,7 @@ version = "0.13.3" description = "Check PEP-8 naming conventions, plugin for flake8" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, @@ -899,6 +989,7 @@ version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, @@ -914,6 +1005,7 @@ version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, @@ -929,6 +1021,7 @@ version = "3.4.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, @@ -947,6 +1040,7 @@ version = "4.4.0" description = "Some out-of-the-box hooks for pre-commit." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pre_commit_hooks-4.4.0-py2.py3-none-any.whl", hash = "sha256:fc8837335476221ccccda3d176ed6ae29fe58753ce7e8b7863f5d0f987328fc6"}, {file = "pre_commit_hooks-4.4.0.tar.gz", hash = "sha256:7011eed8e1a25cde94693da009cba76392194cecc2f3f06c51a44ea6ad6c2af9"}, @@ -962,6 +1056,7 @@ version = "2.9.1" description = "Python style guide checker" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, @@ -973,6 +1068,7 @@ version = "6.3.0" description = "Python docstring style checker" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, @@ -982,7 +1078,7 @@ files = [ snowballstemmer = ">=2.2.0" [package.extras] -toml = ["tomli (>=1.2.3)"] +toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] [[package]] name = "pyflakes" @@ -990,6 +1086,7 @@ version = "2.5.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, @@ -1001,13 +1098,14 @@ version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] [package.extras] -plugins = ["importlib-metadata"] +plugins = ["importlib-metadata ; python_version < \"3.8\""] [[package]] name = "pyparsing" @@ -1015,6 +1113,7 @@ version = "3.1.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" +groups = ["dev"] files = [ {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, @@ -1029,6 +1128,7 @@ version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, @@ -1045,23 +1145,13 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytz" -version = "2023.3.post1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, -] - [[package]] name = "pyupgrade" version = "3.8.0" description = "A tool to automatically upgrade syntax for newer versions." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyupgrade-3.8.0-py2.py3-none-any.whl", hash = "sha256:08d0e6129f5e9da7e7a581bdbea689e0d49c3c93eeaf156a07ae2fd794f52660"}, {file = "pyupgrade-3.8.0.tar.gz", hash = "sha256:1facb0b8407cca468dfcc1d13717e3a85aa37b9e6e7338664ad5bfe5ef50c867"}, @@ -1076,6 +1166,7 @@ version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, @@ -1095,6 +1186,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1135,6 +1227,7 @@ version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, @@ -1156,6 +1249,7 @@ version = "1.4.0" description = "reStructuredText linter" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"}, ] @@ -1169,6 +1263,7 @@ version = "13.5.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" +groups = ["dev"] files = [ {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, @@ -1177,7 +1272,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -1188,6 +1282,7 @@ version = "0.17.32" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3" +groups = ["dev"] files = [ {file = "ruamel.yaml-0.17.32-py3-none-any.whl", hash = "sha256:23cd2ed620231677564646b0c6a89d138b6822a0d78656df7abda5879ec4f447"}, {file = "ruamel.yaml-0.17.32.tar.gz", hash = "sha256:ec939063761914e14542972a5cba6d33c23b0859ab6342f61cf070cfc600efc2"}, @@ -1206,6 +1301,8 @@ version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false python-versions = ">=3.5" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\"" files = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, @@ -1252,6 +1349,7 @@ version = "2.3.5" description = "Checks installed dependencies for known vulnerabilities and licenses." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "safety-2.3.5-py3-none-any.whl", hash = "sha256:2227fcac1b22b53c1615af78872b48348661691450aa25d6704a5504dbd1f7e2"}, {file = "safety-2.3.5.tar.gz", hash = "sha256:a60c11f8952f412cbb165d70cb1f673a3b43a2ba9a93ce11f97e6a4de834aa3a"}, @@ -1275,6 +1373,7 @@ version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, @@ -1282,7 +1381,7 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1291,6 +1390,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1302,6 +1402,7 @@ version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, @@ -1313,6 +1414,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -1324,6 +1426,7 @@ version = "7.1.2" description = "Python documentation generator" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, @@ -1359,6 +1462,7 @@ version = "2021.3.14" description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, @@ -1378,6 +1482,7 @@ version = "5.0.1" description = "Sphinx extension that automatically documents click applications" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinx-click-5.0.1.tar.gz", hash = "sha256:fcc7df15e56e3ff17ebf446cdd316c2eb79580b37c49579fba11e5468802ef25"}, {file = "sphinx_click-5.0.1-py3-none-any.whl", hash = "sha256:31836ca22f746d3c26cbfdfe0c58edf0bca5783731a0b2e25bb6d59800bb75a1"}, @@ -1394,6 +1499,7 @@ version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, @@ -1409,6 +1515,7 @@ version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, @@ -1424,6 +1531,7 @@ version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, @@ -1439,6 +1547,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -1453,6 +1562,7 @@ version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, @@ -1468,6 +1578,7 @@ version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, @@ -1483,6 +1594,7 @@ version = "5.1.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"}, {file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"}, @@ -1497,6 +1609,7 @@ version = "5.2.0" description = "A wrapper around the stdlib `tokenize` which roundtrips." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, @@ -1508,6 +1621,8 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_full_version < \"3.11.0a7\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -1519,6 +1634,7 @@ version = "6.3.3" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">= 3.8" +groups = ["dev"] files = [ {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"}, {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"}, @@ -1539,6 +1655,7 @@ version = "4.1.5" description = "Run-time type checker for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "typeguard-4.1.5-py3-none-any.whl", hash = "sha256:8923e55f8873caec136c892c3bed1f676eae7be57cdb94819281b3d3bc9c0953"}, {file = "typeguard-4.1.5.tar.gz", hash = "sha256:ea0a113bbc111bcffc90789ebb215625c963411f7096a7e9062d4e4630c155fd"}, @@ -1550,7 +1667,7 @@ typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] +test = ["coverage[toml] (>=7)", "mypy (>=1.2.0) ; platform_python_implementation != \"PyPy\"", "pytest (>=7)"] [[package]] name = "typing-extensions" @@ -1558,24 +1675,39 @@ version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + [[package]] name = "urllib3" version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1586,6 +1718,7 @@ version = "20.24.5" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, @@ -1598,7 +1731,7 @@ platformdirs = ">=3.9.1,<4" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "zipp" @@ -1606,6 +1739,8 @@ version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, @@ -1613,9 +1748,12 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-ruff"] + +[extras] +download = ["requests"] [metadata] -lock-version = "2.0" -python-versions = "^3.8" -content-hash = "6f812cae5d4c7f163fb6aec9bbb854ce932edc6f57fc6848b4d61d93be92539b" +lock-version = "2.1" +python-versions = "^3.9" +content-hash = "a236816b9479e6d1eec1ddb8e2435e3c14311e8a4a525027a022fa9d8f2c71eb" diff --git a/pyproject.toml b/pyproject.toml index 34d0489..4ec8127 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hec-dss-python" -version = "0.1.25" +version = "0.1.26" description = "Python wrapper for the HEC-DSS file database C library." authors = ["Hydrologic Engineering Center"] license = "MIT" @@ -10,6 +10,14 @@ packages = [{ include = "hecdss", from = "src" }] [tool.poetry.dependencies] python = "^3.9" +numpy = "^1.20.0" +# tzdata is needed for zoneinfo support on Windows +tzdata = { version = "*", markers = "sys_platform == 'win32'" } +# Optional dependency for downloading HEC-DSS binaries +requests = { version = "^2.25.0", optional = true } + +[tool.poetry.extras] +download = ["requests"] [tool.poetry.dev-dependencies] # Python Syntax highlighter, assume @@ -68,4 +76,4 @@ build-backend = "setuptools.build_meta" log_cli = true log_cli_level = 'INFO' testpaths = ["tests"] -pythonpath = "./src" +pythonpath = ["./src", "./tests"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..90e1d99 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# Core dependencies for hec-dss-python +numpy>=1.20.0 + +# Required for timezone support on Windows +tzdata; sys_platform == 'win32' + +# Optional: For downloading HEC-DSS binary libraries +# Uncomment if you need to download the DLL/SO files: +requests>=2.25.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index e771f80..c5635c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = hecdss -version = 0.1.25 +version = 0.1.26 author = Hydrologic Engineering Center author_email =hec.dss@usace.army.mil description = Python wrapper for the HEC-DSS file database C library. @@ -19,8 +19,8 @@ packages = find: python_requires = >=3.8 include_package_data = True install_requires = - numpy - tzdata + numpy>=1.20.0 + tzdata; sys_platform == 'win32' [options.packages.find] where=src diff --git a/src/hecdss/hecdss.py b/src/hecdss/hecdss.py index 7ad694e..5a59d86 100644 --- a/src/hecdss/hecdss.py +++ b/src/hecdss/hecdss.py @@ -28,17 +28,21 @@ class HecDss: """ Main class for working with DSS files """ - def __init__(self, filename:str, enable_dll_logging:bool = False): + def __init__(self, filename:str, enable_dll_logging:bool = False, message_level:int = 0): """constructor for HecDSS Args: filename (str): DSS filename to be opened; it will be created if it doesn't exist. enable_dll_logging (bool): If True, capture DLL messages to Python logging. Default False. + message_level (int): DLL message level (0-15). Default 0 (silent). """ self._native = _Native() self._dll_monitor = None + # Set DLL message level (0 = silent by default) + self._native.hec_dss_set_debug_level(message_level) + # Setup DLL logging if requested if enable_dll_logging: self._setup_dll_logging() @@ -75,14 +79,17 @@ def _setup_dll_logging(self): # Create temp log file log_file_path = self._dll_monitor.setup_temp_log_file() - # Tell DLL to use this log file + # Tell DLL to use this log file (requires zopenLog function in DLL) status = self._native.hec_dss_open_log_file(log_file_path) + if status == 0: - # Start monitoring + # zopenLog worked, monitor the file self._dll_monitor.start_monitoring() logger.debug("DLL logging enabled, monitoring: %s", log_file_path) else: - logger.warning("Could not enable DLL log file, messages will not be captured") + # zopenLog not available, can't capture DLL output to file + logger.warning("zopenLog not available in DLL, cannot capture DLL output to file") + self._dll_monitor.stop_monitoring() self._dll_monitor = None @staticmethod def set_global_debug_level(level: int) -> None: diff --git a/src/hecdss/logging_config.py b/src/hecdss/logging_config.py index d194692..7d226ca 100644 --- a/src/hecdss/logging_config.py +++ b/src/hecdss/logging_config.py @@ -205,7 +205,7 @@ def configure_logging( class DllLogMonitor: """ Monitors HEC-DSS log file and routes messages to Python logging. - Uses a temporary log file approach to capture DLL output. + Uses a temporary log file approach to capture DLL output when zopenLog is available. """ def __init__(self, logger: logging.Logger): diff --git a/src/hecdss/native.py b/src/hecdss/native.py index b9136fe..b86363f 100644 --- a/src/hecdss/native.py +++ b/src/hecdss/native.py @@ -91,6 +91,17 @@ def __hec_dss_set_value(self, name: str, value: int): f.restype = c_int f(name.encode("utf-8"), value) + def __hec_dss_set_string(self, name: str, value: str): + """Set a string parameter via zset.""" + try: + f = self.dll.hec_dss_set_string + f.argtypes = [c_char_p, c_char_p] + f.restype = c_int + return f(name.encode("utf-8"), value.encode("utf-8")) + except AttributeError: + logger.warning("hec_dss_set_string function not found in DLL") + return -1 + # set debug level (0-15) # 0 - no output # 15 - max output diff --git a/tests/test_array.py b/tests/test_array.py index f6bae63..7bf1903 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -3,7 +3,7 @@ import unittest import numpy as np -from file_manager import FileManager +from .file_manager import FileManager from hecdss import ArrayContainer, HecDss diff --git a/tests/test_basics.py b/tests/test_basics.py index e379f33..619bfa9 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -3,7 +3,7 @@ import unittest from datetime import datetime -from file_manager import FileManager +from .file_manager import FileManager from hecdss import Catalog, HecDss diff --git a/tests/test_dll.log b/tests/test_dll.log new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_gridded_data.py b/tests/test_gridded_data.py index 2ecb94b..e482f35 100644 --- a/tests/test_gridded_data.py +++ b/tests/test_gridded_data.py @@ -3,7 +3,7 @@ import unittest import numpy as np -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss from hecdss.gridded_data import GriddedData diff --git a/tests/test_irregular_timeseries.py b/tests/test_irregular_timeseries.py index bb4aede..8364834 100644 --- a/tests/test_irregular_timeseries.py +++ b/tests/test_irregular_timeseries.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta import numpy as np -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss from hecdss.irregular_timeseries import IrregularTimeSeries diff --git a/tests/test_location_info.py b/tests/test_location_info.py index 9bdb9d9..c53aa5a 100644 --- a/tests/test_location_info.py +++ b/tests/test_location_info.py @@ -2,7 +2,7 @@ import unittest -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss from hecdss.location_info import LocationInfo diff --git a/tests/test_paired_data.py b/tests/test_paired_data.py index b3c9121..7f8ea3a 100644 --- a/tests/test_paired_data.py +++ b/tests/test_paired_data.py @@ -4,7 +4,7 @@ import unittest import numpy as np -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss from hecdss.paired_data import PairedData diff --git a/tests/test_python.log b/tests/test_python.log new file mode 100644 index 0000000..e93dfde --- /dev/null +++ b/tests/test_python.log @@ -0,0 +1,4 @@ +2025-09-19 12:44:35,136 - hecdss.test - DEBUG - Debug message to Python log file +2025-09-19 12:44:35,137 - hecdss.test - INFO - Info message to Python log file +2025-09-19 12:44:52,258 - hecdss.test - DEBUG - Debug message to Python log file +2025-09-19 12:44:52,259 - hecdss.test - INFO - Info message to Python log file diff --git a/tests/test_regular_timeseries.py b/tests/test_regular_timeseries.py index cf078d3..8363d91 100644 --- a/tests/test_regular_timeseries.py +++ b/tests/test_regular_timeseries.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import numpy as np -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss from hecdss.regular_timeseries import RegularTimeSeries diff --git a/tests/test_text.py b/tests/test_text.py index a33ee73..c350bc1 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -2,7 +2,7 @@ import unittest -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss from hecdss.record_type import RecordType diff --git a/tests/workshop.py b/tests/workshop.py index de02cd8..1d703ea 100644 --- a/tests/workshop.py +++ b/tests/workshop.py @@ -2,7 +2,7 @@ import copy -from file_manager import FileManager +from .file_manager import FileManager from hecdss import HecDss From af8ba2dbf0e878f4c4b5fd969099f1d76a596f05 Mon Sep 17 00:00:00 2001 From: oskarhurst <48417811+oskarhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:18:56 -0700 Subject: [PATCH 3/4] update to pytest --- src/hecdss/hecdss.py | 2 + tests/test_dll.log | 0 tests/test_logging.py | 485 +++++++++++++++++++++--------------------- tests/test_python.log | 4 - 4 files changed, 239 insertions(+), 252 deletions(-) delete mode 100644 tests/test_dll.log delete mode 100644 tests/test_python.log diff --git a/src/hecdss/hecdss.py b/src/hecdss/hecdss.py index 5a59d86..362dd87 100644 --- a/src/hecdss/hecdss.py +++ b/src/hecdss/hecdss.py @@ -91,6 +91,7 @@ def _setup_dll_logging(self): logger.warning("zopenLog not available in DLL, cannot capture DLL output to file") self._dll_monitor.stop_monitoring() self._dll_monitor = None + @staticmethod def set_global_debug_level(level: int) -> None: """ @@ -114,6 +115,7 @@ def set_global_debug_level(level: int) -> None: from hecdss.logging_config import get_dll_monitor monitor = get_dll_monitor() monitor.set_dll_level(level) + def close(self): """closes the DSS file and releases any locks """ diff --git a/tests/test_dll.log b/tests/test_dll.log deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_logging.py b/tests/test_logging.py index 0a7567f..3e2596b 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,320 +1,309 @@ -#!/usr/bin/env python -""" -Test script to demonstrate the new logging functionality in hec-dss-python. -""" +"""Pytest module for testing logging functionality in hec-dss-python.""" import logging -import sys import os +import io +import tempfile +import unittest +from contextlib import redirect_stdout, redirect_stderr -# Add src to path for testing -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) +from .file_manager import FileManager from hecdss import ( HecDss, configure_logging, - DLL_MESSAGE + DLL_MESSAGE, + setup_default_logging, + DssPath ) -def test_basic_logging(): - """Test basic logging configuration.""" - print("\n=== Test 1: Basic Logging Configuration ===") - - # Configure to show Python INFO and DLL general messages - configure_logging( - python_level=logging.INFO, - dll_level=3, # MESS_LEVEL_GENERAL - format_string='%(name)s - %(levelname)s - %(message)s' - ) - - # Get a logger and test it - logger = logging.getLogger('hecdss.test') - logger.info("This is an INFO message from Python code") - logger.debug("This DEBUG message should not appear (level too low)") - - print("Basic logging test completed.\n") - - -def test_separate_levels(): - """Test separate control of Python and DLL logging.""" - print("\n=== Test 2: Separate Python and DLL Levels ===") - - # Show only errors from Python but diagnostic messages from DLL - configure_logging( - python_level=logging.ERROR, - dll_level=4 # MESS_LEVEL_USER_DIAG - ) - - logger = logging.getLogger('hecdss.test') - logger.info("This INFO should NOT appear") - logger.error("This ERROR should appear") - - # Test setting DLL level directly - HecDss.set_global_debug_level(5) # MESS_LEVEL_INTERNAL_DIAG_1 - print("DLL debug level set to 5 (internal diagnostics)\n") +class TestLogging(unittest.TestCase): + + def setUp(self) -> None: + """Set up test environment before each test.""" + self.test_files = FileManager() + # Clear any existing logging handlers, closing them first + root_logger = logging.getLogger('hecdss') + dll_logger = logging.getLogger('hecdss.dll') + + # Close file handlers before clearing + for handler in root_logger.handlers[:]: + handler.close() + root_logger.removeHandler(handler) + + for handler in dll_logger.handlers[:]: + handler.close() + dll_logger.removeHandler(handler) + + # Reset DLL debug level to silent + HecDss.set_global_debug_level(0) + + def tearDown(self) -> None: + """Clean up after each test.""" + self.test_files.cleanup() + + # Close any remaining handlers + root_logger = logging.getLogger('hecdss') + dll_logger = logging.getLogger('hecdss.dll') + + for handler in root_logger.handlers[:]: + handler.close() + root_logger.removeHandler(handler) + + for handler in dll_logger.handlers[:]: + handler.close() + dll_logger.removeHandler(handler) + + # Clean up any log files created during tests + for log_file in ['test_python.log', 'test_dll.log']: + if os.path.exists(log_file): + try: + os.remove(log_file) + except PermissionError: + pass # File might still be in use + + def test_basic_logging(self): + """Test basic logging configuration.""" + # Configure to show Python INFO and DLL general messages + configure_logging( + python_level=logging.INFO, + dll_level=3, # MESS_LEVEL_GENERAL + format_string='%(name)s - %(levelname)s - %(message)s' + ) + + # Get a logger and test it + logger = logging.getLogger('hecdss.test') + # Capture log output + with self.assertLogs('hecdss.test', level=logging.INFO) as cm: + logger.info("This is an INFO message from Python code") + logger.debug("This DEBUG message should not appear (level too low)") -def test_file_logging(): - """Test logging to files.""" - print("\n=== Test 3: File Logging ===") + # Check that only INFO message was logged + self.assertEqual(len(cm.output), 1) + self.assertIn("INFO", cm.output[0]) + self.assertIn("This is an INFO message", cm.output[0]) - # Configure separate files for Python and DLL - configure_logging( - python_file='test_python.log', - dll_file='test_dll.log', - python_level=logging.DEBUG, - dll_level=4, - console=False # Don't output to console - ) + def test_separate_levels(self): + """Test separate control of Python and DLL logging.""" + # Show only errors from Python but diagnostic messages from DLL + configure_logging( + python_level=logging.ERROR, + dll_level=4 # MESS_LEVEL_USER_DIAG + ) - logger = logging.getLogger('hecdss.test') - logger.debug("Debug message to Python log file") - logger.info("Info message to Python log file") + logger = logging.getLogger('hecdss.test') - print("Check test_python.log and test_dll.log files for output.\n") + # INFO should not appear, but ERROR should + with self.assertLogs('hecdss.test', level=logging.ERROR) as cm: + logger.info("This INFO should NOT appear") + logger.error("This ERROR should appear") + + self.assertEqual(len(cm.output), 1) + self.assertIn("ERROR", cm.output[0]) + self.assertIn("This ERROR should appear", cm.output[0]) + + def test_file_logging(self): + """Test logging to files.""" + # Use temporary directory for log files + with tempfile.TemporaryDirectory() as tmpdir: + python_log = os.path.join(tmpdir, 'test_python.log') + dll_log = os.path.join(tmpdir, 'test_dll.log') + + # Configure separate files for Python and DLL + configure_logging( + python_file=python_log, + dll_file=dll_log, + python_level=logging.DEBUG, + dll_level=4, + console=False # Don't output to console + ) + logger = logging.getLogger('hecdss.test') + logger.debug("Debug message to Python log file") + logger.info("Info message to Python log file") -def test_custom_levels(): - """Test custom DLL message level.""" - print("\n=== Test 4: Custom DLL Message Level ===") + # Check that log file was created and has content + self.assertTrue(os.path.exists(python_log)) + with open(python_log, 'r') as f: + content = f.read() + self.assertIn("Debug message", content) + self.assertIn("Info message", content) - # Configure to show DLL messages - configure_logging( - dll_level=4, # Set DLL to user diagnostic level - format_string='%(levelname)-15s - %(message)s' - ) + # Important: Close and remove all file handlers before exiting + root_logger = logging.getLogger('hecdss') + dll_logger = logging.getLogger('hecdss.dll') - # Get DLL logger to demonstrate the single DLL level - dll_logger = logging.getLogger('hecdss.dll') + handlers_to_remove = [] - # Use the single DLL message method - dll_logger.dll_message("This is a DLL message") - dll_logger.dll_message("All DLL messages use the same Python log level") - dll_logger.dll_message("The DLL itself controls what gets written based on its debug level") + # Collect all file handlers + for handler in root_logger.handlers: + if isinstance(handler, logging.FileHandler): + handlers_to_remove.append((root_logger, handler)) - print("Custom level test completed.\n") + for handler in dll_logger.handlers: + if isinstance(handler, logging.FileHandler): + handlers_to_remove.append((dll_logger, handler)) + # Close and remove them + for logger_ref, handler in handlers_to_remove: + handler.close() + logger_ref.removeHandler(handler) -def test_no_logging(): - """Test that logging can be completely disabled.""" - print("\n=== Test 5: No Logging (Default Behavior) ===") + def test_custom_dll_message_level(self): + """Test custom DLL message level.""" + # Configure to show DLL messages + configure_logging( + dll_level=4, # Set DLL to user diagnostic level + format_string='%(levelname)-15s - %(message)s' + ) - # Reset to default (no output) - root_logger = logging.getLogger('hecdss') - dll_logger = logging.getLogger('hecdss.dll') + # Get DLL logger to demonstrate the single DLL level + dll_logger = logging.getLogger('hecdss.dll') - # Clear handlers - root_logger.handlers.clear() - dll_logger.handlers.clear() + # Test that dll_message method exists and works + with self.assertLogs('hecdss.dll', level=DLL_MESSAGE) as cm: + dll_logger.dll_message("This is a DLL message") + dll_logger.dll_message("All DLL messages use the same Python log level") - # Add only NullHandler (no output) - root_logger.addHandler(logging.NullHandler()) + self.assertEqual(len(cm.output), 2) + self.assertIn("DLL", cm.output[0]) - logger = logging.getLogger('hecdss.test') - logger.info("This should not appear anywhere") - logger.error("This error should also not appear") + def test_no_logging_by_default(self): + """Test that logging can be completely disabled.""" + # Reset to default (no output) + setup_default_logging() - print("No output should have appeared from logging calls.") - print("This maintains backward compatibility - no output by default.\n") + logger = logging.getLogger('hecdss.test') + # Should not raise because of NullHandler + logger.info("This should not appear anywhere") + logger.error("This error should also not appear") -def test_with_dss_file(): - """Test logging with actual DSS file operations if available.""" - print("\n=== Test 6: DSS File Operations with Logging ===") + # Check that root logger has NullHandler + root_logger = logging.getLogger('hecdss') + self.assertTrue(any(isinstance(h, logging.NullHandler) for h in root_logger.handlers)) - # Configure logging to see what's happening - configure_logging( - python_level=logging.INFO, - dll_level=3, # General log messages - format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) + def test_with_dss_file(self): + """Test logging with actual DSS file operations.""" + # Configure logging to see what's happening + configure_logging( + python_level=logging.INFO, + dll_level=3, # General log messages + format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) - test_file = "test_logging.dss" + test_file = self.test_files.create_test_file(".dss") - try: - # This will test the logging in actual DSS operations with HecDss(test_file) as dss: logger = logging.getLogger('hecdss.test') - logger.info(f"Successfully opened {test_file}") - - # Get catalog (this might trigger some logging) - catalog = dss.get_catalog() - logger.info(f"Catalog has {len(catalog.items)} items") - - # Test the new log_items method if catalog has items - if catalog.items: - catalog.log_items(logging.INFO) - - except Exception as e: - logger = logging.getLogger('hecdss.test') - logger.error(f"Error during DSS operations: {e}") - - # Clean up test file if it was created - if os.path.exists(test_file): - os.remove(test_file) - print(f"Cleaned up {test_file}") - - print("DSS file test completed.\n") + with self.assertLogs('hecdss', level=logging.INFO) as cm: + logger.info(f"Successfully opened {test_file}") + catalog = dss.get_catalog() + logger.info(f"Catalog has {len(catalog.items)} items") -def test_dll_output_capture(): - """Test capturing DLL output to Python logging.""" - print("\n=== Test 7: DLL Output Capture ===") + # Should have at least the two info messages + self.assertGreaterEqual(len(cm.output), 2) - # Configure logging with DLL output capture - configure_logging( - python_level=logging.INFO, - dll_level=4, # User diagnostic messages - format_string='%(name)-20s - %(levelname)-10s - %(message)s', - capture_dll_output=True # Enable DLL capture setup - ) + def test_dll_output_capture_without_zopen_log(self): + """Test that DLL output capture fails gracefully without zopenLog.""" + # Configure logging with DLL output capture + configure_logging( + python_level=logging.INFO, + dll_level=4, # User diagnostic messages + capture_dll_output=True # Enable DLL capture setup + ) - test_file = "test_dll_capture.dss" + test_file = self.test_files.create_test_file(".dss") - try: # Create HecDss with DLL logging enabled - with HecDss(test_file, enable_dll_logging=True) as dss: - logger = logging.getLogger('hecdss.test') - logger.info(f"Opened {test_file} with DLL logging enabled") - - # Set higher debug level to see more DLL messages - HecDss.set_global_debug_level(5) # Internal diagnostics level 1 - - # Perform operations that should trigger DLL messages + # This should fail gracefully if zopenLog is not available + with HecDss(test_file, enable_dll_logging=True, message_level=0) as dss: + # Should work without errors even if zopenLog isn't available catalog = dss.get_catalog() - logger.info(f"Retrieved catalog with {len(catalog.items)} items") - - # The DLL messages should now appear in the Python logs with [DLL] prefix - - except Exception as e: - logger = logging.getLogger('hecdss.test') - logger.error(f"Error during DLL capture test: {e}") - - # Clean up - if os.path.exists(test_file): - os.remove(test_file) - print(f"Cleaned up {test_file}") - - print("DLL output capture test completed.\n") - - -def test_silent_by_default(): - """Test that hec-dss-python produces NO output by default.""" - print("\n=== Test 8: Silent by Default (Best Practice) ===") + self.assertIsNotNone(catalog) - import io - from contextlib import redirect_stdout, redirect_stderr + def test_silent_by_default(self): + """Test that hec-dss-python produces NO output by default.""" + # Capture all output + stdout_capture = io.StringIO() + stderr_capture = io.StringIO() - # Capture all output - stdout_capture = io.StringIO() - stderr_capture = io.StringIO() + test_file = self.test_files.create_test_file(".dss") - test_file = "test_silent.dss" - - try: with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture): - # Fresh import simulation - clear any existing handlers + # Fresh setup - clear any existing handlers root_logger = logging.getLogger('hecdss') dll_logger = logging.getLogger('hecdss.dll') root_logger.handlers.clear() dll_logger.handlers.clear() # Re-setup default (what happens on import) - from hecdss import setup_default_logging setup_default_logging() # Use the library normally - dss = HecDss(test_file) - catalog = dss.get_catalog() - - # Try logging - should produce nothing - logger = logging.getLogger('hecdss') - logger.info("This should not appear") - logger.error("This should not appear either") - - # DLL logger too - dll_logger = logging.getLogger('hecdss.dll') - dll_logger.dll_message("This DLL message should not appear") - - dss.close() - - finally: - # Clean up - if os.path.exists(test_file): - os.remove(test_file) - - # Get captured output - stdout_text = stdout_capture.getvalue() - stderr_text = stderr_capture.getvalue() - - # Report results - if stdout_text == "" and stderr_text == "": - print("✓ PASS: No output produced by default (follows Python best practices)") - else: - print("✗ FAIL: Unexpected output detected!") - if stdout_text: - print(f" stdout: {repr(stdout_text[:100])}") - if stderr_text: - print(f" stderr: {repr(stderr_text[:100])}") - - print("This ensures the library is silent unless explicitly configured.\n") + with HecDss(test_file, message_level=0) as dss: + catalog = dss.get_catalog() + # Try logging - should produce nothing + logger = logging.getLogger('hecdss') + logger.info("This should not appear") + logger.error("This should not appear either") -def test_explicit_print_still_works(): - """Test that explicit print methods work when called.""" - print("\n=== Test 9: Explicit print() Methods Still Work ===") + # DLL logger too + dll_logger = logging.getLogger('hecdss.dll') + dll_logger.dll_message("This DLL message should not appear") - import io - from contextlib import redirect_stdout - from hecdss import DssPath + # Get captured output + stdout_text = stdout_capture.getvalue() + stderr_text = stderr_capture.getvalue() - stdout_capture = io.StringIO() + # Should be completely silent + self.assertEqual(stdout_text, "") + self.assertEqual(stderr_text, "") - with redirect_stdout(stdout_capture): - # Create a path and explicitly call print - path = DssPath("/A/B/C/01Jan2024/1Hour/F/") - path.print() # This SHOULD produce output + def test_explicit_print_still_works(self): + """Test that explicit print methods work when called.""" + stdout_capture = io.StringIO() - output = stdout_capture.getvalue() + with redirect_stdout(stdout_capture): + # Create a path and explicitly call print + path = DssPath("/A/B/C/01Jan2024/1Hour/F/") + path.print() # This SHOULD produce output - # Should see the path parts - if "a:" in output and "b:" in output: - print("✓ PASS: Explicit print() methods work when called") - else: - print("✗ FAIL: print() method did not produce expected output") - print(f" Output: {repr(output[:100])}") + output = stdout_capture.getvalue() - print("Users can still use print() methods when they want output.\n") + # Should see the path parts + self.assertIn("a:", output) + self.assertIn("b:", output) + self.assertIn("c:", output) + def test_set_global_debug_level(self): + """Test setting global DLL debug level.""" + # This should work without errors + HecDss.set_global_debug_level(0) # Silent + HecDss.set_global_debug_level(3) # General -def main(): - """Run all logging tests.""" - print("=" * 60) - print("HEC-DSS Python Logging Test Suite") - print("=" * 60) + # Test higher levels briefly + HecDss.set_global_debug_level(6) # Full debug + # Immediately set back to silent to avoid debug output + HecDss.set_global_debug_level(0) - # Run tests - test_basic_logging() - test_separate_levels() - test_file_logging() - test_custom_levels() - test_no_logging() - test_with_dss_file() - test_dll_output_capture() - test_silent_by_default() - test_explicit_print_still_works() + # Test that level is clamped to valid range + HecDss.set_global_debug_level(-1) # Should become 0 + HecDss.set_global_debug_level(20) # Should become 15 + # Set back to silent + HecDss.set_global_debug_level(0) - print("=" * 60) - print("All tests completed!") - print("=" * 60) + # No assertions needed - just checking it doesn't crash - # Clean up any log files created - for log_file in ['test_python.log', 'test_dll.log']: - if os.path.exists(log_file): - os.remove(log_file) - print(f"Cleaned up {log_file}") + def test_dll_message_level_enum(self): + """Test that DLL_MESSAGE level is properly defined.""" + self.assertEqual(DLL_MESSAGE, 25) + self.assertTrue(logging.INFO < DLL_MESSAGE < logging.WARNING) if __name__ == "__main__": - main() \ No newline at end of file + unittest.main() \ No newline at end of file diff --git a/tests/test_python.log b/tests/test_python.log deleted file mode 100644 index e93dfde..0000000 --- a/tests/test_python.log +++ /dev/null @@ -1,4 +0,0 @@ -2025-09-19 12:44:35,136 - hecdss.test - DEBUG - Debug message to Python log file -2025-09-19 12:44:35,137 - hecdss.test - INFO - Info message to Python log file -2025-09-19 12:44:52,258 - hecdss.test - DEBUG - Debug message to Python log file -2025-09-19 12:44:52,259 - hecdss.test - INFO - Info message to Python log file From 072702190f7a7d4e9be2821cb5dfd6fe04fd9668 Mon Sep 17 00:00:00 2001 From: oskarhurst <48417811+oskarhurst@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:36:22 -0700 Subject: [PATCH 4/4] remove dot --- tests/test_array.py | 2 +- tests/test_basics.py | 2 +- tests/test_gridded_data.py | 2 +- tests/test_irregular_timeseries.py | 2 +- tests/test_location_info.py | 2 +- tests/test_logging.py | 2 +- tests/test_paired_data.py | 2 +- tests/test_regular_timeseries.py | 2 +- tests/test_text.py | 2 +- tests/workshop.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_array.py b/tests/test_array.py index 7bf1903..f6bae63 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -3,7 +3,7 @@ import unittest import numpy as np -from .file_manager import FileManager +from file_manager import FileManager from hecdss import ArrayContainer, HecDss diff --git a/tests/test_basics.py b/tests/test_basics.py index 619bfa9..e379f33 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -3,7 +3,7 @@ import unittest from datetime import datetime -from .file_manager import FileManager +from file_manager import FileManager from hecdss import Catalog, HecDss diff --git a/tests/test_gridded_data.py b/tests/test_gridded_data.py index e482f35..2ecb94b 100644 --- a/tests/test_gridded_data.py +++ b/tests/test_gridded_data.py @@ -3,7 +3,7 @@ import unittest import numpy as np -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss from hecdss.gridded_data import GriddedData diff --git a/tests/test_irregular_timeseries.py b/tests/test_irregular_timeseries.py index 8364834..bb4aede 100644 --- a/tests/test_irregular_timeseries.py +++ b/tests/test_irregular_timeseries.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta import numpy as np -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss from hecdss.irregular_timeseries import IrregularTimeSeries diff --git a/tests/test_location_info.py b/tests/test_location_info.py index c53aa5a..9bdb9d9 100644 --- a/tests/test_location_info.py +++ b/tests/test_location_info.py @@ -2,7 +2,7 @@ import unittest -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss from hecdss.location_info import LocationInfo diff --git a/tests/test_logging.py b/tests/test_logging.py index 3e2596b..dd72661 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -7,7 +7,7 @@ import unittest from contextlib import redirect_stdout, redirect_stderr -from .file_manager import FileManager +from file_manager import FileManager from hecdss import ( HecDss, diff --git a/tests/test_paired_data.py b/tests/test_paired_data.py index 7f8ea3a..b3c9121 100644 --- a/tests/test_paired_data.py +++ b/tests/test_paired_data.py @@ -4,7 +4,7 @@ import unittest import numpy as np -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss from hecdss.paired_data import PairedData diff --git a/tests/test_regular_timeseries.py b/tests/test_regular_timeseries.py index 8363d91..cf078d3 100644 --- a/tests/test_regular_timeseries.py +++ b/tests/test_regular_timeseries.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import numpy as np -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss from hecdss.regular_timeseries import RegularTimeSeries diff --git a/tests/test_text.py b/tests/test_text.py index c350bc1..a33ee73 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -2,7 +2,7 @@ import unittest -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss from hecdss.record_type import RecordType diff --git a/tests/workshop.py b/tests/workshop.py index 1d703ea..de02cd8 100644 --- a/tests/workshop.py +++ b/tests/workshop.py @@ -2,7 +2,7 @@ import copy -from .file_manager import FileManager +from file_manager import FileManager from hecdss import HecDss