pyadps 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ import re
6
+ import io
7
+ import contextlib
8
+
9
+ import configparser
10
+ import streamlit as st
11
+ from utils.autoprocess import autoprocess
12
+ from utils.multifile import ADCPBinFileCombiner
13
+
14
+ # To make the page wider if the user presses the reload button.
15
+ st.set_page_config(layout="wide")
16
+
17
+
18
+ def ansi_to_html(text):
19
+ """
20
+ Function to convert ANSI (console color) to HTML.
21
+ To display the text, map the output to st.markdown
22
+ """
23
+ text = re.sub(r"\x1b\[31m", "<span style='color:red'><br>", text) # red
24
+ text = re.sub(r"\x1b\[32m", "<span style='color:green'><br>", text) # red
25
+ text = re.sub(r"\x1b\[33m", "<span style='color:orange'><br>", text) # green
26
+ text = re.sub(r"\x1b\[0m", "</span>", text) # reset
27
+ return text
28
+
29
+
30
+ @st.cache_data
31
+ def file_access(uploaded_file):
32
+ """
33
+ Function creates temporary directory to store the uploaded file.
34
+ The path of the file is returned
35
+
36
+ Args:
37
+ uploaded_file (string): Name of the uploaded file
38
+
39
+ Returns:
40
+ path (string): Path of the uploaded file
41
+ """
42
+ temp_dir = tempfile.mkdtemp()
43
+ path = os.path.join(temp_dir, uploaded_file.name)
44
+ with open(path, "wb") as f:
45
+ f.write(uploaded_file.getvalue())
46
+ return path
47
+
48
+
49
+ def display_config_as_json(config_file):
50
+ config = configparser.ConfigParser()
51
+ config.read_string(config_file.getvalue().decode("utf-8"))
52
+ st.json({section: dict(config[section]) for section in config.sections()})
53
+
54
+
55
+ def main():
56
+ st.title("🧰 Add-Ons")
57
+ st.header("🔧 Auto Processing Tool", divider=True)
58
+ st.write(
59
+ "You can use a configuration file from `pyadps` to re-process ADCP data by simply adjusting threshold values within the file. "
60
+ "This allows you to fine-tune the output without repeating the full processing workflow in the software."
61
+ )
62
+ st.write(
63
+ "To begin, upload both a binary ADCP file and a `config.ini` file for processing."
64
+ )
65
+
66
+ # File Upload Section
67
+ uploaded_binary_file = st.file_uploader(
68
+ "Upload ADCP Binary File", type=["000", "bin"]
69
+ )
70
+ uploaded_config_file = st.file_uploader(
71
+ "Upload Config File (config.ini)", type=["ini"]
72
+ )
73
+
74
+ if uploaded_binary_file and uploaded_config_file:
75
+ st.success("Files uploaded successfully!")
76
+
77
+ # Display config.ini file content as JSON
78
+ display_config_as_json(uploaded_config_file)
79
+
80
+ fpath = file_access(uploaded_binary_file)
81
+ # Process files
82
+ with st.spinner("Processing files. Please wait..."):
83
+ autoprocess(uploaded_config_file, binary_file_path=fpath)
84
+ st.success("Processing completed successfully!")
85
+ st.write("Processed file written.")
86
+
87
+ st.header("🔗 Binary File Combiner", divider=True)
88
+ st.write(
89
+ "ADCPs may produce multiple binary segments instead of a single continuous file. "
90
+ "This tool scans each uploaded binary file for the `7f7f` header, removes any broken ensembles at the beginning or the end, and combines all valid segments into a single file. "
91
+ "To ensure correct order during concatenation, please rename the files using sequential numbering. "
92
+ "For example: `KKS_000.000`, `KKS_001.000`, `KKS_002.000`."
93
+ )
94
+ output_cat_filename = "merged_000.000"
95
+ st.info(f"Current file name: **{output_cat_filename}**")
96
+ output_cat_filename_radio = st.radio(
97
+ "Would you like to edit the output filename?",
98
+ ["No", "Yes"],
99
+ horizontal=True,
100
+ )
101
+ if output_cat_filename_radio == "Yes":
102
+ output_cat_filename = st.text_input(
103
+ "Enter file name (e.g., GD10A000)",
104
+ value=output_cat_filename,
105
+ )
106
+
107
+ display_log = st.radio(
108
+ "Display log from console:",
109
+ ["No", "Yes"],
110
+ horizontal=True,
111
+ )
112
+
113
+ uploaded_files = st.file_uploader(
114
+ "Upload multiple binary files", type=["bin", "000"], accept_multiple_files=True
115
+ )
116
+
117
+ if uploaded_files:
118
+ st.info("Saving uploaded files to temporary disk files...")
119
+
120
+ # Save files to temporary path
121
+ temp_file_paths = []
122
+ for uploaded_file in uploaded_files:
123
+ suffix = Path(uploaded_file.name).suffix
124
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
125
+ tmp.write(uploaded_file.read())
126
+ temp_path = Path(tmp.name)
127
+ temp_file_paths.append(temp_path)
128
+
129
+ st.divider()
130
+ st.subheader("🛠 Processing and Combining...")
131
+
132
+ if display_log == "Yes":
133
+ # The `buffer` is used to display console output to streamlit
134
+ buffer = io.StringIO()
135
+ with contextlib.redirect_stdout(buffer):
136
+ adcpcat = ADCPBinFileCombiner()
137
+ combined_data = adcpcat.combine_files(temp_file_paths)
138
+ st.markdown(ansi_to_html(buffer.getvalue()), unsafe_allow_html=True)
139
+ else:
140
+ adcpcat = ADCPBinFileCombiner()
141
+ combined_data = adcpcat.combine_files(temp_file_paths)
142
+
143
+ if combined_data:
144
+ st.success("✅ Valid binary data has been combined successfully.")
145
+ st.warning(
146
+ "⚠️ Note: The time axis in the final file may be irregular due to missing ensembles during concatenation."
147
+ )
148
+ st.download_button(
149
+ label="📥 Download Combined Binary File",
150
+ data=bytes(combined_data),
151
+ file_name=output_cat_filename,
152
+ mime="application/octet-stream",
153
+ )
154
+ else:
155
+ st.warning("⚠️ No valid data found to combine.")
156
+
157
+ # Optional: Clean up temporary files
158
+ for path in temp_file_paths:
159
+ try:
160
+ os.remove(path)
161
+ except Exception as e:
162
+ st.warning(f"Failed to delete temp file {path}: {e}")
163
+ else:
164
+ st.info("Please upload binary files to begin.")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
pyadps/utils/__init__.py CHANGED
@@ -9,4 +9,6 @@ from pyadps.utils.signal_quality import *
9
9
  from pyadps.utils.velocity_test import *
10
10
  from pyadps.utils.writenc import *
11
11
  from pyadps.utils.autoprocess import *
12
+ from pyadps.utils.logging_utils import *
13
+ from pyadps.utils.multifile import *
12
14
  from pyadps.utils.script import *
@@ -0,0 +1,269 @@
1
+ """
2
+ Reusable Logging Utilities
3
+ A clean, configurable logging module that can be used across multiple projects.
4
+ """
5
+
6
+ import logging
7
+ import sys
8
+ from enum import Enum
9
+ from typing import Optional, Union
10
+ from pathlib import Path
11
+
12
+
13
+ class LogLevel(Enum):
14
+ """Log level enumeration"""
15
+
16
+ DEBUG = logging.DEBUG
17
+ INFO = logging.INFO
18
+ WARNING = logging.WARNING
19
+ ERROR = logging.ERROR
20
+ CRITICAL = logging.CRITICAL
21
+
22
+
23
+ class CustomFormatter(logging.Formatter):
24
+ """Custom colored formatter for console logging"""
25
+
26
+ COLORS = {
27
+ logging.DEBUG: "\x1b[36m", # Cyan
28
+ logging.INFO: "\x1b[32m", # Green
29
+ logging.WARNING: "\x1b[33m", # Yellow
30
+ logging.ERROR: "\x1b[31m", # Red
31
+ logging.CRITICAL: "\x1b[31;1m", # Bold Red
32
+ }
33
+ RESET = "\x1b[0m"
34
+
35
+ def __init__(self, include_timestamp: bool = True, include_module: bool = False):
36
+ """
37
+ Initialize formatter with optional components
38
+
39
+ Args:
40
+ include_timestamp: Whether to include timestamp in log format
41
+ include_module: Whether to include module name in log format
42
+ """
43
+ self.include_timestamp = include_timestamp
44
+ self.include_module = include_module
45
+ super().__init__()
46
+
47
+ def format(self, record):
48
+ """Format log record with colors and optional components"""
49
+ # Build format string based on options
50
+ format_parts = []
51
+
52
+ if self.include_timestamp:
53
+ format_parts.append("%(asctime)s")
54
+
55
+ format_parts.append("%(levelname)s")
56
+
57
+ if self.include_module:
58
+ format_parts.append("%(name)s")
59
+
60
+ format_parts.append("%(message)s")
61
+
62
+ log_format = " - ".join(format_parts)
63
+
64
+ # Apply color
65
+ color = self.COLORS.get(record.levelno, "")
66
+ colored_format = color + log_format + self.RESET
67
+
68
+ formatter = logging.Formatter(
69
+ colored_format,
70
+ datefmt="%Y-%m-%d %H:%M:%S" if self.include_timestamp else None,
71
+ )
72
+ return formatter.format(record)
73
+
74
+
75
+ class LoggerConfig:
76
+ """Configuration class for logger setup"""
77
+
78
+ def __init__(
79
+ self,
80
+ level: LogLevel = LogLevel.INFO,
81
+ include_timestamp: bool = True,
82
+ include_module: bool = False,
83
+ log_to_file: bool = False,
84
+ log_file_path: Optional[Union[str, Path]] = None,
85
+ file_log_level: Optional[LogLevel] = None,
86
+ max_file_size: int = 10 * 1024 * 1024, # 10MB
87
+ backup_count: int = 5,
88
+ ):
89
+ """
90
+ Initialize logger configuration
91
+
92
+ Args:
93
+ level: Console logging level
94
+ include_timestamp: Include timestamp in console output
95
+ include_module: Include module name in output
96
+ log_to_file: Whether to also log to file
97
+ log_file_path: Path for log file (if log_to_file is True)
98
+ file_log_level: File logging level (defaults to console level)
99
+ max_file_size: Maximum size of log file before rotation
100
+ backup_count: Number of backup files to keep
101
+ """
102
+ self.level = level
103
+ self.include_timestamp = include_timestamp
104
+ self.include_module = include_module
105
+ self.log_to_file = log_to_file
106
+ self.log_file_path = Path(log_file_path) if log_file_path else None
107
+ self.file_log_level = file_log_level or level
108
+ self.max_file_size = max_file_size
109
+ self.backup_count = backup_count
110
+
111
+
112
+ class LoggerManager:
113
+ """Manages logger configuration and setup"""
114
+
115
+ _loggers = {} # Cache for created loggers
116
+
117
+ @classmethod
118
+ def setup_logger(
119
+ self, name: str = "app", config: Optional[LoggerConfig] = None
120
+ ) -> logging.Logger:
121
+ """
122
+ Set up and configure logger with given configuration
123
+
124
+ Args:
125
+ name: Logger name
126
+ config: Logger configuration (uses defaults if None)
127
+
128
+ Returns:
129
+ Configured logger instance
130
+ """
131
+ # Use default config if none provided
132
+ if config is None:
133
+ config = LoggerConfig()
134
+
135
+ # Return cached logger if it exists
136
+ cache_key = f"{name}_{id(config)}"
137
+ if cache_key in self._loggers:
138
+ return self._loggers[cache_key]
139
+
140
+ logger = logging.getLogger(name)
141
+ logger.setLevel(config.level.value)
142
+
143
+ # Remove existing handlers to avoid duplicates
144
+ logger.handlers.clear()
145
+
146
+ # Create console handler
147
+ console_handler = logging.StreamHandler(sys.stdout)
148
+ console_handler.setLevel(config.level.value)
149
+ console_formatter = CustomFormatter(
150
+ include_timestamp=config.include_timestamp,
151
+ include_module=config.include_module,
152
+ )
153
+ console_handler.setFormatter(console_formatter)
154
+ logger.addHandler(console_handler)
155
+
156
+ # Add file handler if requested
157
+ if config.log_to_file and config.log_file_path:
158
+ self._add_file_handler(logger, config)
159
+
160
+ # Prevent propagation to root logger
161
+ logger.propagate = False
162
+
163
+ # Cache the logger
164
+ self._loggers[cache_key] = logger
165
+
166
+ return logger
167
+
168
+ @classmethod
169
+ def _add_file_handler(self, logger: logging.Logger, config: LoggerConfig):
170
+ """Add rotating file handler to logger"""
171
+ from logging.handlers import RotatingFileHandler
172
+
173
+ # Ensure log directory exists
174
+ config.log_file_path.parent.mkdir(parents=True, exist_ok=True)
175
+
176
+ file_handler = RotatingFileHandler(
177
+ config.log_file_path,
178
+ maxBytes=config.max_file_size,
179
+ backupCount=config.backup_count,
180
+ )
181
+ file_handler.setLevel(config.file_log_level.value)
182
+
183
+ # File logs typically include more detail
184
+ file_formatter = logging.Formatter(
185
+ "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
186
+ datefmt="%Y-%m-%d %H:%M:%S",
187
+ )
188
+ file_handler.setFormatter(file_formatter)
189
+ logger.addHandler(file_handler)
190
+
191
+ @classmethod
192
+ def get_logger(self, name: str = "app") -> logging.Logger:
193
+ """Get existing logger or create with default config"""
194
+ return logging.getLogger(name) or self.setup_logger(name)
195
+
196
+ @classmethod
197
+ def clear_cache(self):
198
+ """Clear logger cache (useful for testing)"""
199
+ self._loggers.clear()
200
+
201
+
202
+ # Convenience functions for quick setup
203
+ def get_console_logger(
204
+ name: str = "app", level: LogLevel = LogLevel.INFO, include_timestamp: bool = True
205
+ ) -> logging.Logger:
206
+ """Quick setup for console-only logger"""
207
+ config = LoggerConfig(
208
+ level=level, include_timestamp=include_timestamp, include_module=False
209
+ )
210
+ return LoggerManager.setup_logger(name, config)
211
+
212
+
213
+ def get_file_logger(
214
+ name: str = "app",
215
+ log_file: Union[str, Path] = "app.log",
216
+ level: LogLevel = LogLevel.INFO,
217
+ file_level: Optional[LogLevel] = None,
218
+ ) -> logging.Logger:
219
+ """Quick setup for file + console logger"""
220
+ config = LoggerConfig(
221
+ level=level,
222
+ log_to_file=True,
223
+ log_file_path=log_file,
224
+ file_log_level=file_level or LogLevel.DEBUG,
225
+ )
226
+ return LoggerManager.setup_logger(name, config)
227
+
228
+
229
+ def get_detailed_logger(
230
+ name: str = "app",
231
+ log_file: Union[str, Path] = "app.log",
232
+ console_level: LogLevel = LogLevel.INFO,
233
+ file_level: LogLevel = LogLevel.DEBUG,
234
+ ) -> logging.Logger:
235
+ """Setup logger with detailed configuration"""
236
+ config = LoggerConfig(
237
+ level=console_level,
238
+ include_timestamp=True,
239
+ include_module=True,
240
+ log_to_file=True,
241
+ log_file_path=log_file,
242
+ file_log_level=file_level,
243
+ )
244
+ return LoggerManager.setup_logger(name, config)
245
+
246
+
247
+ # Example usage
248
+ if __name__ == "__main__":
249
+ # Test different logger configurations
250
+
251
+ # Simple console logger
252
+ simple_logger = get_console_logger("simple", LogLevel.DEBUG)
253
+ simple_logger.debug("Debug message")
254
+ simple_logger.info("Info message")
255
+ simple_logger.warning("Warning message")
256
+ simple_logger.error("Error message")
257
+
258
+ print("\n" + "=" * 50 + "\n")
259
+
260
+ # File + console logger
261
+ file_logger = get_file_logger("file_test", "test.log", LogLevel.INFO)
262
+ file_logger.info("This goes to both console and file")
263
+ file_logger.debug("This only goes to file")
264
+
265
+ print("\n" + "=" * 50 + "\n")
266
+
267
+ # Detailed logger
268
+ detailed_logger = get_detailed_logger("detailed", "detailed.log")
269
+ detailed_logger.info("Detailed logging with module names")
@@ -0,0 +1,244 @@
1
+ """
2
+ ADCP (Acoustic Doppler Current Profiler) File Processor
3
+ A clean, maintainable implementation for processing and combining ADCP binary files.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from dataclasses import dataclass
8
+ from typing import List, Union
9
+
10
+ # Import from our separate logging module
11
+ from .logging_utils import LogLevel, get_console_logger
12
+
13
+
14
+ @dataclass
15
+ class ADCPConfig:
16
+ """Configuration for ADCP file processing"""
17
+
18
+ file_extension: str = "*.000"
19
+ header_signature: bytes = b"\x7f\x7f"
20
+ header_signature_ext: bytes = b"\x7f\x7f\xf0\x02"
21
+ ensemble_size_offset: int = 2
22
+ ensemble_size_length: int = 2
23
+ header_size_adjustment: int = 2
24
+ chunk_size: int = 8192 # For large file processing
25
+
26
+
27
+ class ADCPError(Exception):
28
+ """Base exception for ADCP processing errors"""
29
+
30
+ pass
31
+
32
+
33
+ class InvalidHeaderError(ADCPError):
34
+ """Raised when ADCP file has invalid header"""
35
+
36
+ pass
37
+
38
+
39
+ class CorruptedFileError(ADCPError):
40
+ """Raised when ADCP file is corrupted"""
41
+
42
+ pass
43
+
44
+
45
+ class ADCPFileValidator:
46
+ """Validates ADCP files and headers"""
47
+
48
+ def __init__(self, config: ADCPConfig, logger_name: str = "adcp_validator"):
49
+ self.config = config
50
+ self.logger = get_console_logger(logger_name, LogLevel.INFO)
51
+
52
+ def find_header_start(self, data: bytes) -> int:
53
+ """Find the first occurrence of the extended header signature"""
54
+ return data.find(self.config.header_signature_ext)
55
+
56
+ def validate_file_path(self, filepath: Path) -> None:
57
+ """Validate file path exists and is accessible"""
58
+ if not filepath.exists():
59
+ raise FileNotFoundError(f"File {filepath} does not exist")
60
+ if not filepath.is_file():
61
+ raise ValueError(f"Path {filepath} is not a file")
62
+ if filepath.stat().st_size == 0:
63
+ raise ValueError(f"File {filepath} is empty")
64
+
65
+ def has_valid_header(self, data: bytes) -> bool:
66
+ """Check if data starts with valid ADCP header"""
67
+ return data.startswith(self.config.header_signature)
68
+
69
+
70
+ class ADCPFileProcessor:
71
+ """Processes individual ADCP files"""
72
+
73
+ def __init__(self, config: ADCPConfig = None, logger_name: str = "adcp_processor"):
74
+ self.config = config or ADCPConfig()
75
+ self.validator = ADCPFileValidator(self.config, f"{logger_name}_validator")
76
+ self.logger = get_console_logger(logger_name, LogLevel.INFO)
77
+
78
+ def _calculate_ensemble_size(self, data: bytes) -> int:
79
+ """Calculate size of single ensemble from header"""
80
+ offset = self.config.ensemble_size_offset
81
+ length = self.config.ensemble_size_length
82
+ return (
83
+ int.from_bytes(data[offset : offset + length], byteorder="little")
84
+ + self.config.header_size_adjustment
85
+ )
86
+
87
+ def _validate_file_integrity(
88
+ self, filepath: Path, data: bytes, ensemble_size: int
89
+ ) -> int:
90
+ """Validate file integrity and return number of valid ensembles"""
91
+ file_size = filepath.stat().st_size
92
+ if file_size % ensemble_size != 0:
93
+ valid_ensembles = file_size // ensemble_size
94
+ self.logger.warning(
95
+ f"File {filepath.name} is corrupted. "
96
+ f"Valid ensembles: {valid_ensembles}/{valid_ensembles + 1}"
97
+ )
98
+ return valid_ensembles
99
+ return file_size // ensemble_size
100
+
101
+ def process_file(self, filepath: Union[str, Path]) -> bytes:
102
+ """Process a single ADCP file and return valid data"""
103
+ filepath = Path(filepath)
104
+ try:
105
+ self.validator.validate_file_path(filepath)
106
+
107
+ with open(filepath, "rb") as f:
108
+ data = f.read()
109
+
110
+ header_index = 0
111
+ # Check if file starts with valid header
112
+ if not self.validator.has_valid_header(data):
113
+ header_index = self.validator.find_header_start(data)
114
+ if header_index == -1:
115
+ raise InvalidHeaderError(
116
+ f"File {filepath.name} contains no valid ADCP header"
117
+ )
118
+ self.logger.warning(
119
+ f"File {filepath.name} header found at byte {header_index}. "
120
+ "Truncating invalid data before header."
121
+ )
122
+ else:
123
+ self.logger.info(f"Valid ADCP file: {filepath.name}")
124
+
125
+ # Calculate ensemble size and validate file integrity
126
+ ensemble_size = self._calculate_ensemble_size(data[header_index:])
127
+ valid_ensembles = self._validate_file_integrity(
128
+ filepath, data, ensemble_size
129
+ )
130
+
131
+ # Return only valid data
132
+ end_index = header_index + (valid_ensembles * ensemble_size)
133
+ return data[header_index:end_index]
134
+
135
+ except (InvalidHeaderError, FileNotFoundError, ValueError) as e:
136
+ self.logger.error(f"Error processing {filepath.name}: {e}")
137
+ return b""
138
+ except Exception as e:
139
+ self.logger.error(f"Unexpected error processing {filepath.name}: {e}")
140
+ return b""
141
+
142
+
143
+ class ADCPBinFileCombiner:
144
+ """Combines or joins multiple ADCP files"""
145
+
146
+ def __init__(self, config: ADCPConfig = None, logger_name: str = "adcp_combiner"):
147
+ self.config = config or ADCPConfig()
148
+ self.processor = ADCPFileProcessor(self.config, f"{logger_name}_processor")
149
+ self.logger = get_console_logger(logger_name, LogLevel.INFO)
150
+
151
+ def get_adcp_files(self, folder_path: Union[str, Path]) -> List[Path]:
152
+ """Get all ADCP files from folder"""
153
+ folder_path = Path(folder_path)
154
+ if not folder_path.exists():
155
+ raise FileNotFoundError(f"Folder {folder_path} does not exist")
156
+ if not folder_path.is_dir():
157
+ raise NotADirectoryError(f"Path {folder_path} is not a directory")
158
+
159
+ files = sorted(folder_path.glob(self.config.file_extension))
160
+ if not files:
161
+ self.logger.error(
162
+ f"No {self.config.file_extension} files found in {folder_path}"
163
+ )
164
+ return files
165
+
166
+ def combine_files(self, files: List[Union[str, Path]]) -> bytearray:
167
+ """Combine multiple ADCP files into single bytearray"""
168
+ if not files:
169
+ self.logger.warning("No files provided for combination")
170
+ return bytearray()
171
+
172
+ combined_data = bytearray()
173
+ processed_count = 0
174
+
175
+ for file_path in files:
176
+ valid_data = self.processor.process_file(file_path)
177
+ if valid_data:
178
+ combined_data.extend(valid_data)
179
+ processed_count += 1
180
+
181
+ self.logger.info(f"Successfully combined {processed_count}/{len(files)} files")
182
+ return combined_data
183
+
184
+ def combine_folder(
185
+ self, folder_path: Union[str, Path], output_file: Union[str, Path]
186
+ ) -> bool:
187
+ """Combine all ADCP files from folder and write to output file"""
188
+ try:
189
+ files = self.get_adcp_files(folder_path)
190
+ if not files:
191
+ self.logger.error("No valid files found to combine")
192
+ return False
193
+
194
+ combined_data = self.combine_files(files)
195
+ if not combined_data:
196
+ self.logger.error("No valid data to write")
197
+ return False
198
+
199
+ output_path = Path(output_file)
200
+ output_path.parent.mkdir(parents=True, exist_ok=True)
201
+
202
+ with open(output_path, "wb") as f:
203
+ f.write(combined_data)
204
+
205
+ self.logger.info(
206
+ # f"Successfully combined {len(files)} files. "
207
+ f"Output written to: {output_path} ({len(combined_data)} bytes)"
208
+ )
209
+ return True
210
+
211
+ except Exception as e:
212
+ self.logger.error(f"Error combining folder {folder_path}: {e}")
213
+ return False
214
+
215
+
216
+ def main():
217
+ """Main entry point for CLI usage"""
218
+ try:
219
+ folder = input("Enter folder containing ADCP files (*.000): ").strip()
220
+ if not folder:
221
+ print("No folder specified. Exiting.")
222
+ return
223
+
224
+ output = input("Enter output filename (default: merged_000.000): ").strip()
225
+ if not output:
226
+ output = "merged_000.000"
227
+
228
+ # Create combiner with custom logger configuration
229
+ combiner = ADCPBinFileCombiner(logger_name="adcp_main")
230
+ success = combiner.combine_folder(folder, output)
231
+
232
+ if success:
233
+ print(f"✅ Files successfully combined to {output}")
234
+ else:
235
+ print("❌ Failed to combine files. Check logs for details.")
236
+
237
+ except KeyboardInterrupt:
238
+ print("\n⚠️ Operation cancelled by user.")
239
+ except Exception as e:
240
+ print(f"❌ Unexpected error: {e}")
241
+
242
+
243
+ if __name__ == "__main__":
244
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyadps
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A Python package for ADCP data processing
5
5
  Home-page: https://pyadps.readthedocs.io/en/latest/index.html
6
6
  License: MIT
@@ -9,14 +9,16 @@ pyadps/pages/05_QC_Test.py,sha256=6YjQTg_t0_Qll9xQ3SYieCBS2cDEMz9znovW3olPFwc,16
9
9
  pyadps/pages/06_Profile_Test.py,sha256=Vir91oRIWApbO2elBm4I59rdf83NtspUmtzAyWdsIiY,34891
10
10
  pyadps/pages/07_Velocity_Test.py,sha256=K4vEiLPMXrU4JMLj-mIA1G4H5ORozMbHMiMov3ZZXP0,23008
11
11
  pyadps/pages/08_Write_File.py,sha256=dehT7x68b0bzDDiTSE7W0cav4JdvoB1Awicip711hGY,23079
12
- pyadps/pages/09_Auto_process.py,sha256=SRtQVD9_kodlSvYdF9-02ur6EaWG2zMvN6-BcWdzYV8,1874
12
+ pyadps/pages/09_Add-Ons.py,sha256=lweofAxcvKLxzfCJl6vybmUmxWgcm4jEn5DjFaTHbpI,5986
13
13
  pyadps/pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- pyadps/utils/__init__.py,sha256=nCqRp-OT_1BC7RnL3ARUIldlw9sWyr1XqQvQid-B4ts,407
14
+ pyadps/utils/__init__.py,sha256=JDIy5Yx6meDGxfLGyB5135TFWQc0lyNxu_cWrfSEDhU,485
15
15
  pyadps/utils/autoprocess.py,sha256=r6SZwx-FPV9Fp1DzuLfH06X5zStWVIi94182juebxfA,20374
16
+ pyadps/utils/logging_utils.py,sha256=-b6rwnpF-g6ZSIj3PVyxiz4-blBKvKqh1HW1KRKpmyY,8492
16
17
  pyadps/utils/metadata/config.ini,sha256=TC7htzGwUukIXt_u3JR5ycyvOoDj_JxWgGY6khjNeck,2154
17
18
  pyadps/utils/metadata/demo.000,sha256=qxB3sgjABrpv4DNXkwjpbSxk5sc4UwAI8kgQX0--PM8,234468
18
19
  pyadps/utils/metadata/flmeta.json,sha256=eGw1lM5IKhZ6IuSkj7PiKQ-MpNtH446HaRmcby8rBIY,12673
19
20
  pyadps/utils/metadata/vlmeta.json,sha256=_dkQlGkkUvpAIM7S6kEUenSaiCpOrwXg8n1aU3dDF3s,22535
21
+ pyadps/utils/multifile.py,sha256=NKSbikp6k98pHgT-5tciAyykYCTyWfPH1JFA9F8eiUs,8747
20
22
  pyadps/utils/plotgen.py,sha256=c7TUA9unErUxppYc14LZ2o_stzNMmmZ5X542iI5v1tA,26346
21
23
  pyadps/utils/profile_test.py,sha256=gnbS6ZsqKvv2tcHTj-Fi_VNOszbxDcPxl77_n4dLzSo,29237
22
24
  pyadps/utils/pyreadrdi.py,sha256=VwypCpKGfjTO0h0j_Vl1-JMhXooiaXnmQHmEFVPifGo,36886
@@ -26,8 +28,8 @@ pyadps/utils/sensor_health.py,sha256=aHRaU4kMJZ9dGmYypKpCCgq-owWoNjvcl1I_9I7dG68
26
28
  pyadps/utils/signal_quality.py,sha256=BYPAbXrAPGQBEfEyG4PFktYpqxuvhoafsWM172TVn08,16737
27
29
  pyadps/utils/velocity_test.py,sha256=O8dgjv_5pxhJq6QuWHxysMjNzxSnob_2KPLInmO1kHI,6112
28
30
  pyadps/utils/writenc.py,sha256=KDolQ11Whh2bi4L2wfrgjXym5BWL8ikp0xvo4Pa0r4E,14455
29
- pyadps-0.1.3.dist-info/LICENSE,sha256=sfY_7DzQF5FxnO2T6ek74dfm5uBmwEp1oEg_WlzNsb8,1092
30
- pyadps-0.1.3.dist-info/METADATA,sha256=F49hGNEXebr0Q8M0ul29c6HoB4ColJFetU03OGinEgg,4518
31
- pyadps-0.1.3.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
32
- pyadps-0.1.3.dist-info/entry_points.txt,sha256=-oZhbbJq8Q29uNVh5SmzOLp9OeFM9VUzHVxovfI4LXA,126
33
- pyadps-0.1.3.dist-info/RECORD,,
31
+ pyadps-0.1.4.dist-info/LICENSE,sha256=sfY_7DzQF5FxnO2T6ek74dfm5uBmwEp1oEg_WlzNsb8,1092
32
+ pyadps-0.1.4.dist-info/METADATA,sha256=M9Qsr-K3w1VHOMajGLAw8CVlhJzD2gz7oqW_er-Jwak,4518
33
+ pyadps-0.1.4.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
34
+ pyadps-0.1.4.dist-info/entry_points.txt,sha256=-oZhbbJq8Q29uNVh5SmzOLp9OeFM9VUzHVxovfI4LXA,126
35
+ pyadps-0.1.4.dist-info/RECORD,,
@@ -1,64 +0,0 @@
1
- import os
2
- import tempfile
3
-
4
- import configparser
5
- import json
6
- import streamlit as st
7
- from utils.autoprocess import autoprocess
8
-
9
- # To make the page wider if the user presses the reload button.
10
- st.set_page_config(layout="wide")
11
-
12
- @st.cache_data
13
- def file_access(uploaded_file):
14
- """
15
- Function creates temporary directory to store the uploaded file.
16
- The path of the file is returned
17
-
18
- Args:
19
- uploaded_file (string): Name of the uploaded file
20
-
21
- Returns:
22
- path (string): Path of the uploaded file
23
- """
24
- temp_dir = tempfile.mkdtemp()
25
- path = os.path.join(temp_dir, uploaded_file.name)
26
- with open(path, "wb") as f:
27
- f.write(uploaded_file.getvalue())
28
- return path
29
-
30
-
31
- def display_config_as_json(config_file):
32
- config = configparser.ConfigParser()
33
- config.read_string(config_file.getvalue().decode("utf-8"))
34
- st.json({section: dict(config[section]) for section in config.sections()})
35
-
36
-
37
- def main():
38
- st.title("ADCP Data Auto Processing Tool")
39
- st.write("Upload a binary input file and config.ini file for processing.")
40
-
41
- # File Upload Section
42
- uploaded_binary_file = st.file_uploader(
43
- "Upload ADCP Binary File", type=["000", "bin"]
44
- )
45
- uploaded_config_file = st.file_uploader(
46
- "Upload Config File (config.ini)", type=["ini"]
47
- )
48
-
49
- if uploaded_binary_file and uploaded_config_file:
50
- st.success("Files uploaded successfully!")
51
-
52
- # Display config.ini file content as JSON
53
- display_config_as_json(uploaded_config_file)
54
-
55
- fpath = file_access(uploaded_binary_file)
56
- # Process files
57
- with st.spinner("Processing files. Please wait..."):
58
- autoprocess(uploaded_config_file, binary_file_path=fpath)
59
- st.success("Processing completed successfully!")
60
- st.write("Processed file written.")
61
-
62
-
63
- if __name__ == "__main__":
64
- main()
File without changes