diff --git a/.github/workflows/build-wheels-cibuildwheel.yml b/.github/workflows/build-wheels-cibuildwheel.yml new file mode 100644 index 0000000..894c582 --- /dev/null +++ b/.github/workflows/build-wheels-cibuildwheel.yml @@ -0,0 +1,154 @@ +name: Build Wheels (cibuildwheel) + +on: + push: + branches: + - main + - master + tags: + - 'v*' + pull_request: + branches: + - main + - master + workflow_dispatch: + +env: + CIBW_BUILD: cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* + CIBW_SKIP: "*-musllinux_* *-win32 *-manylinux_i686" + CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_ARCHS_LINUX: "x86_64 aarch64" + CIBW_ARCHS_WINDOWS: "AMD64" + # Build with maturin + CIBW_BEFORE_BUILD: pip install maturin + CIBW_BUILD_FRONTEND: build + +jobs: + build-wheels-linux: + name: Build wheels on Linux + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, aarch64] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up QEMU + if: matrix.target == 'aarch64' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16 + with: + package-dir: rbufrp + output-dir: wheelhouse + env: + CIBW_ARCHS_LINUX: ${{ matrix.target }} + + - uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.target }} + path: ./wheelhouse/*.whl + if-no-files-found: error + + build-wheels-macos: + name: Build wheels on macOS + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, arm64] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16 + with: + package-dir: rbufrp + output-dir: wheelhouse + env: + CIBW_ARCHS_MACOS: ${{ matrix.target }} + + - uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.target }} + path: ./wheelhouse/*.whl + if-no-files-found: error + + build-wheels-windows: + name: Build wheels on Windows + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16 + with: + package-dir: rbufrp + output-dir: wheelhouse + + - uses: actions/upload-artifact@v4 + with: + name: wheels-windows-amd64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install maturin build + + - name: Build sdist + working-directory: rbufrp + run: maturin sdist --out dist + + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: sdist + path: rbufrp/dist/*.tar.gz + if-no-files-found: error + + publish-to-github-release: + name: Publish to GitHub Release + needs: [build-wheels-linux, build-wheels-macos, build-wheels-windows, build-sdist] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: dist/* + generate_release_notes: true + draft: false + prerelease: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..12e4792 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: + +jobs: + rust-test: + name: Rust Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo check + run: cargo check --all --verbose + + - name: Run cargo test + run: cargo test --all --verbose + + - name: Run cargo clippy + run: cargo clippy --all -- -D warnings + + - name: Run cargo fmt check + run: cargo fmt --all -- --check + + python-test: + name: Python Tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.8', '3.11', '3.13'] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install maturin + run: pip install maturin pytest + + - name: Build Python package + working-directory: rbufrp + run: maturin develop --release + + - name: Test Python package + working-directory: rbufrp + run: | + python -c "import rbufrp; print(rbufrp.__version__)" + python -c "import rbufrp; print(rbufrp.get_tables_path())" + + check-formatting: + name: Check Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install ruff + run: pip install ruff + + - name: Check Python formatting with ruff + run: | + ruff check rbufrp/src/rbufrp/ + ruff format --check rbufrp/src/rbufrp/ diff --git a/BUFR4 b/BUFR4 new file mode 160000 index 0000000..a6b7ab0 --- /dev/null +++ b/BUFR4 @@ -0,0 +1 @@ +Subproject commit a6b7ab078d4c70c69565655f7cf7c7d3913d6d78 diff --git a/rbufr/Cargo.toml b/rbufr/Cargo.toml index 43b40bc..c78eb0b 100644 --- a/rbufr/Cargo.toml +++ b/rbufr/Cargo.toml @@ -21,6 +21,7 @@ rustc-hash = "2.1.1" [features] default = ["opera"] opera = ["gentools/opera"] +python_bindings = [] [profile.bench] diff --git a/rbufr/src/decoder.rs b/rbufr/src/decoder.rs index c8f49dc..3f63202 100644 --- a/rbufr/src/decoder.rs +++ b/rbufr/src/decoder.rs @@ -11,7 +11,11 @@ use genlib::{ prelude::{BUFRTableB, BUFRTableBitMap, BUFRTableD}, tables::{ArchivedBTableEntry, ArchivedDTableEntry}, }; -use std::{fmt::Display, ops::Deref}; +use std::{ + borrow::{Borrow, Cow}, + fmt::Display, + ops::Deref, +}; const MISS_VAL: f64 = 99999.999999; @@ -1376,6 +1380,7 @@ impl<'a, 'b> Container<'a> for Repeating<'a, 'b> { } } +#[derive(Clone)] pub struct BUFRParsed<'a> { records: Vec>, } @@ -1385,11 +1390,11 @@ impl<'a> BUFRParsed<'a> { Self { records: vec![] } } - pub fn push(&mut self, value: Value, element_name: &'a str, unit: &'a str) { + fn push(&mut self, value: Value, element_name: &'a str, unit: &'a str) { self.records.push(BUFRRecord { - name: Some(element_name), + name: Some(Cow::Borrowed(element_name)), values: BUFRData::Single(value), - unit: Some(unit), + unit: Some(Cow::Borrowed(unit)), }); } @@ -1406,6 +1411,12 @@ impl<'a> BUFRParsed<'a> { values: Vec::with_capacity(time), } } + + pub fn into_owned(&self) -> BUFRParsed<'static> { + BUFRParsed { + records: self.records.iter().map(|r| r.into_owned()).collect(), + } + } } struct Array<'a, 's> { @@ -1424,9 +1435,9 @@ impl<'a> Array<'a, '_> { fn finish(self, name: Option<&'a str>, unit: Option<&'a str>) { let recording = BUFRRecord { - name, + name: name.map(|n| Cow::Borrowed(n)), values: BUFRData::Array(self.values), - unit, + unit: unit.map(|u| Cow::Borrowed(u)), }; self.parsed.records.push(recording); } @@ -1452,21 +1463,38 @@ impl<'a, 's> Repeating<'a, 's> { } } +#[derive(Debug, Clone)] pub enum BUFRData { Repeat(Vec), Single(Value), Array(Vec), } +#[derive(Clone)] pub struct BUFRRecord<'a> { - pub name: Option<&'a str>, + // pub name: Option<&'a str>, + pub name: Option>, pub values: BUFRData, - pub unit: Option<&'a str>, + pub unit: Option>, +} + +impl BUFRRecord<'_> { + pub fn into_owned(&self) -> BUFRRecord<'static> { + BUFRRecord { + name: self.name.as_ref().map(|s| Cow::Owned(s.to_string())), + values: match &self.values { + BUFRData::Single(v) => BUFRData::Single(v.clone()), + BUFRData::Repeat(vs) => BUFRData::Repeat(vs.clone()), + BUFRData::Array(a) => BUFRData::Array(a.clone()), + }, + unit: self.unit.as_ref().map(|s| Cow::Owned(s.to_string())), + } + } } impl Display for BUFRRecord<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let is_print_unit = match self.unit { + let is_print_unit = match self.unit.as_ref().map(|s| &**s) { Some("CAITT IA5" | "code table" | "code-table" | "flag table" | "flag-table") => false, None => false, _ => true, @@ -1477,79 +1505,33 @@ impl Display for BUFRRecord<'_> { } let name = self.name.as_ref().unwrap(); + let width = f.width().unwrap_or(0); match &self.values { BUFRData::Single(v) => { - if is_print_unit { - write!(f, "{}: {} {}", name, v, self.unit.as_ref().unwrap())?; + if width > 0 { + write!(f, "{: write!(f, "MISSING")?, + Value::String(s) => write!(f, "\"{}\"", s)?, + Value::Number(n) => { + if is_print_unit { + write!(f, "{:>12.6} {}", n, self.unit.as_ref().unwrap())?; + } else { + write!(f, "{}", n)?; + } + } } } BUFRData::Repeat(vs) => { - if vs.len() < 8 { - write!(f, "{}: [", name)?; - for v in vs { - if is_print_unit { - write!(f, "{} {}, ", v, self.unit.as_ref().unwrap())?; - } else { - write!(f, "{}, ", v)?; - } - } - - write!(f, "]")?; - } else { - write!(f, "{}: [", name)?; - for v in &vs[0..5] { - if is_print_unit { - write!(f, "{} {}, ", v, self.unit.as_ref().unwrap())?; - } else { - write!(f, "{}, ", v)?; - } - } - write!(f, "... ")?; - for v in &vs[vs.len() - 2..vs.len()] { - if is_print_unit { - write!(f, "{} {}, ", v, self.unit.as_ref().unwrap())?; - } else { - write!(f, "{}, ", v)?; - } - } - write!(f, "]")?; - } + self.format_sequence(f, name, vs, is_print_unit, width)?; } - BUFRData::Array(a) => { - if a.len() < 8 { - write!(f, "{}: [", name)?; - for v in a { - if is_print_unit { - write!(f, "{} {}, ", v, self.unit.as_ref().unwrap())?; - } else { - write!(f, "{}, ", v)?; - } - } - - write!(f, "]")?; - } else { - write!(f, "{}: [", name)?; - for v in &a[0..5] { - if is_print_unit { - write!(f, "{} {}, ", v, self.unit.as_ref().unwrap())?; - } else { - write!(f, "{}, ", v)?; - } - } - write!(f, "... ")?; - for v in &a[a.len() - 2..a.len()] { - if is_print_unit { - write!(f, "{} {}, ", v, self.unit.as_ref().unwrap())?; - } else { - write!(f, "{}, ", v)?; - } - } - write!(f, "]")?; - } + self.format_array(f, name, a, is_print_unit, width)?; } } @@ -1557,15 +1539,283 @@ impl Display for BUFRRecord<'_> { } } +impl BUFRRecord<'_> { + fn format_sequence( + &self, + f: &mut std::fmt::Formatter<'_>, + name: &str, + values: &[Value], + is_print_unit: bool, + width: usize, + ) -> std::fmt::Result { + let missing_count = values.iter().filter(|v| v.is_missing()).count(); + + if width > 0 { + write!(f, "{: 0 { + write!(f, ", missing={}", missing_count)?; + } + write!(f, "] ")?; + + if values.is_empty() { + write!(f, "[]")?; + return Ok(()); + } + + let show_limit = 6; + if values.len() <= show_limit { + write!(f, "[")?; + for (i, v) in values.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + self.format_value(f, v, is_print_unit)?; + } + write!(f, "]")?; + } else { + write!(f, "[")?; + for (i, v) in values.iter().take(3).enumerate() { + if i > 0 { + write!(f, ", ")?; + } + self.format_value(f, v, is_print_unit)?; + } + write!(f, " ... ")?; + for (i, v) in values.iter().skip(values.len() - 2).enumerate() { + if i > 0 { + write!(f, ", ")?; + } + self.format_value(f, v, is_print_unit)?; + } + write!(f, "]")?; + } + + Ok(()) + } + + fn format_array( + &self, + f: &mut std::fmt::Formatter<'_>, + name: &str, + values: &[f64], + is_print_unit: bool, + width: usize, + ) -> std::fmt::Result { + let missing_count = values.iter().filter(|&&v| v == MISS_VAL).count(); + let valid_values: Vec = values.iter().copied().filter(|&v| v != MISS_VAL).collect(); + + if width > 0 { + write!(f, "{: 0 { + write!(f, ", missing={}", missing_count)?; + } + + // 显示统计信息 + if !valid_values.is_empty() { + let min = valid_values.iter().copied().fold(f64::INFINITY, f64::min); + let max = valid_values + .iter() + .copied() + .fold(f64::NEG_INFINITY, f64::max); + let mean = valid_values.iter().sum::() / valid_values.len() as f64; + + write!(f, ", min={:.3}, max={:.3}, mean={:.3}", min, max, mean)?; + } + write!(f, "]")?; + + if is_print_unit { + if let Some(unit) = &self.unit { + write!(f, " {}", unit)?; + } + } + + // 显示样例值 + if !values.is_empty() { + let show_limit = 6; + if values.len() <= show_limit { + write!(f, "\n [")?; + for (i, v) in values.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + if *v == MISS_VAL { + write!(f, "MISSING")?; + } else { + write!(f, "{:.3}", v)?; + } + } + write!(f, "]")?; + } else { + write!(f, "\n [")?; + for (i, v) in values.iter().take(3).enumerate() { + if i > 0 { + write!(f, ", ")?; + } + if *v == MISS_VAL { + write!(f, "MISSING")?; + } else { + write!(f, "{:.3}", v)?; + } + } + write!(f, " ... ")?; + for (i, v) in values.iter().skip(values.len() - 2).enumerate() { + if i > 0 { + write!(f, ", ")?; + } + if *v == MISS_VAL { + write!(f, "MISSING")?; + } else { + write!(f, "{:.3}", v)?; + } + } + write!(f, "]")?; + } + } + + Ok(()) + } + + fn format_value( + &self, + f: &mut std::fmt::Formatter<'_>, + value: &Value, + is_print_unit: bool, + ) -> std::fmt::Result { + match value { + Value::Missing => write!(f, "MISSING"), + Value::String(s) => write!(f, "\"{}\"", s), + Value::Number(n) => { + if is_print_unit { + write!(f, "{:.3}", n) + } else { + write!(f, "{}", n) + } + } + } + } +} + impl Display for BUFRParsed<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "BUFR Parsed Data ({} records)", self.records.len())?; + + // 计算最长的名称长度用于对齐 + let max_name_len = self + .records + .iter() + .filter_map(|r| r.name.as_ref()) + .map(|n| n.len()) + .max() + .unwrap_or(0) + .min(50); // 限制最大宽度 + for record in &self.records { + writeln!(f, "{: { + /// 获取记录数量 + pub fn record_count(&self) -> usize { + self.records.len() + } + + /// 获取所有记录 + pub fn records(&self) -> &[BUFRRecord<'_>] { + &self.records + } + + /// 紧凑格式显示(不带边框和统计信息) + pub fn display_compact(&self) -> CompactDisplay<'_> { + CompactDisplay(self) + } + + /// 详细格式显示(包含更多元数据) + pub fn display_detailed(&self) -> DetailedDisplay<'_> { + DetailedDisplay(self) + } +} + +/// 紧凑显示包装器 +pub struct CompactDisplay<'a>(&'a BUFRParsed<'a>); + +impl Display for CompactDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for record in &self.0.records { writeln!(f, "{}", record)?; } Ok(()) } } +/// 详细显示包装器 +pub struct DetailedDisplay<'a>(&'a BUFRParsed<'a>); + +impl Display for DetailedDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "BUFR Parsed Data - Detailed View")?; + writeln!(f)?; + + // 统计信息 + let total_records = self.0.records.len(); + let single_count = self + .0 + .records + .iter() + .filter(|r| matches!(r.values, BUFRData::Single(_))) + .count(); + let array_count = self + .0 + .records + .iter() + .filter(|r| matches!(r.values, BUFRData::Array(_))) + .count(); + let repeat_count = self + .0 + .records + .iter() + .filter(|r| matches!(r.values, BUFRData::Repeat(_))) + .count(); + + writeln!(f, "Statistics:")?; + writeln!(f, " Total records: {}", total_records)?; + writeln!(f, " Single values: {}", single_count)?; + writeln!(f, " Arrays: {}", array_count)?; + writeln!(f, " Repeated values: {}", repeat_count)?; + writeln!(f)?; + + // 详细记录 + let max_name_len = self + .0 + .records + .iter() + .filter_map(|r| r.name.as_ref()) + .map(|n| n.len()) + .max() + .unwrap_or(0) + .min(50); + + for (idx, record) in self.0.records.iter().enumerate() { + writeln!(f, "Record {}: {: { Slice { descs: Descs<'v>, diff --git a/rbufr/src/lib.rs b/rbufr/src/lib.rs index b65e46d..a179114 100644 --- a/rbufr/src/lib.rs +++ b/rbufr/src/lib.rs @@ -5,7 +5,9 @@ pub mod errors; pub mod opera; pub mod parser; pub mod structs; +pub mod table_path; pub mod tables; pub use crate::decoder::Decoder; pub use crate::parser::*; +pub use crate::table_path::{get_tables_base_path, set_tables_base_path}; diff --git a/rbufr/src/table_path.rs b/rbufr/src/table_path.rs new file mode 100644 index 0000000..c1a1240 --- /dev/null +++ b/rbufr/src/table_path.rs @@ -0,0 +1,65 @@ +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +static TABLES_BASE_PATH: OnceLock = OnceLock::new(); + +pub fn set_tables_base_path>(path: P) { + let _ = TABLES_BASE_PATH.set(path.as_ref().to_path_buf()); +} + +pub fn get_tables_base_path() -> PathBuf { + if let Some(path) = TABLES_BASE_PATH.get() { + return path.clone(); + } + + if let Ok(env_path) = std::env::var("RBUFR_TABLES_PATH") { + return PathBuf::from(env_path); + } + + #[cfg(feature = "python_bindings")] + if let Some(python_path) = try_find_python_package_path() { + return python_path; + } + + PathBuf::from("tables") +} + +#[allow(dead_code)] +fn try_find_python_package_path() -> Option { + if let Ok(exe_path) = std::env::current_exe() { + let mut candidate = exe_path.parent()?.to_path_buf(); + candidate.push("rbufrp"); + candidate.push("tables"); + if candidate.exists() { + return Some(candidate); + } + } + None +} + +pub fn get_table_path>(relative_path: P) -> PathBuf { + let base = get_tables_base_path(); + base.join(relative_path) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_set_and_get_path() { + set_tables_base_path("/custom/tables/path"); + let path = get_tables_base_path(); + assert_eq!(path, PathBuf::from("/custom/tables/path")); + } + + #[test] + fn test_get_table_path() { + set_tables_base_path("/base"); + let table_path = get_table_path("master/BUFR_TableB_0.bufrtbl"); + assert_eq!( + table_path, + PathBuf::from("/base/master/BUFR_TableB_0.bufrtbl") + ); + } +} diff --git a/rbufr/src/tables.rs b/rbufr/src/tables.rs index c2566b8..c9e3693 100644 --- a/rbufr/src/tables.rs +++ b/rbufr/src/tables.rs @@ -52,18 +52,16 @@ impl LocalTable { } impl TableTrait for MasterTable { fn file_path(&self, table_type: TableType) -> PathBuf { + use crate::table_path::get_table_path; + match table_type { TableType::B => { - let mut base_dir = PathBuf::new(); - base_dir.push("tables/master"); - let file_name = format!("BUFR_TableB_{}.bufrtbl", self.version); - base_dir.join(file_name) + let file_name = format!("master/BUFR_TableB_{}.bufrtbl", self.version); + get_table_path(file_name) } TableType::D => { - let mut base_dir = PathBuf::new(); - base_dir.push("tables/master"); - let file_name = format!("BUFR_TableD_{}.bufrtbl", self.version); - base_dir.join(file_name) + let file_name = format!("master/BUFR_TableD_{}.bufrtbl", self.version); + get_table_path(file_name) } _ => { unreachable!("Table type not supported for MasterTable") @@ -74,26 +72,24 @@ impl TableTrait for MasterTable { impl TableTrait for LocalTable { fn file_path(&self, table_type: TableType) -> PathBuf { + use crate::table_path::get_table_path; + match table_type { TableType::B => { - let mut base_dir = PathBuf::new(); - base_dir.push("tables/local"); let sub_center_str = match self.sub_center { Some(sc) => format!("{}", sc), None => "0".to_string(), }; - let file_name = format!("BUFR_TableB_{}_{}.bufrtbl", sub_center_str, self.version); - base_dir.join(file_name) + let file_name = format!("local/BUFR_TableB_{}_{}.bufrtbl", sub_center_str, self.version); + get_table_path(file_name) } TableType::D => { - let mut base_dir = PathBuf::new(); - base_dir.push("tables/local"); let sub_center_str = match self.sub_center { Some(sc) => format!("{}", sc), None => "0".to_string(), }; - let file_name = format!("BUFR_TableD_{}_{}.bufrtbl", sub_center_str, self.version); - base_dir.join(file_name) + let file_name = format!("local/BUFR_TableD_{}_{}.bufrtbl", sub_center_str, self.version); + get_table_path(file_name) } _ => { unreachable!("Table type not supported for LocalTable") @@ -104,12 +100,12 @@ impl TableTrait for LocalTable { impl TableTrait for BitmapTable { fn file_path(&self, table_type: TableType) -> PathBuf { + use crate::table_path::get_table_path; + match table_type { TableType::BitMap => { - let mut base_dir = PathBuf::new(); - base_dir.push("tables/opera"); - let file_name = format!("BUFR_Opera_Bitmap_{}.bufrtbl", self.center); - base_dir.join(file_name) + let file_name = format!("opera/BUFR_Opera_Bitmap_{}.bufrtbl", self.center); + get_table_path(file_name) } _ => { unreachable!("Table type not supported for BitmapTable") diff --git a/rbufrp/Cargo.toml b/rbufrp/Cargo.toml index af607b0..7812dd8 100644 --- a/rbufrp/Cargo.toml +++ b/rbufrp/Cargo.toml @@ -13,4 +13,4 @@ crate-type = ["cdylib"] # "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9 pyo3 = { version = "0.27.1", features = ["extension-module", "abi3-py38"] } -rbufr = { path = "../rbufr" } +rbufr = { path = "../rbufr", features = ["python_bindings"] } diff --git a/rbufrp/MANIFEST.in b/rbufrp/MANIFEST.in new file mode 100644 index 0000000..8efc13e --- /dev/null +++ b/rbufrp/MANIFEST.in @@ -0,0 +1 @@ +recursive-include ../rbufr/tables *.bufrtbl diff --git a/rbufrp/pyproject.toml b/rbufrp/pyproject.toml index cb1f34a..e2b7714 100644 --- a/rbufrp/pyproject.toml +++ b/rbufrp/pyproject.toml @@ -3,9 +3,7 @@ name = "rbufrp" version = "0.1.0" description = "Add your description here" readme = "README.md" -authors = [ - { name = "Tsuki", email = "qwin7989@gmail.com" } -] +authors = [{ name = "Tsuki", email = "qwin7989@gmail.com" }] requires-python = ">=3.11" dependencies = [] @@ -16,9 +14,18 @@ rbufrp = "rbufrp:main" module-name = "rbufrp._core" python-packages = ["rbufrp"] python-source = "src" +include = [ + { path = "../rbufr/tables", format = "sdist" }, + { path = "../rbufr/tables", format = "wheel" }, +] [tool.uv] -cache-keys = [{ file = "pyproject.toml" }, { file = "src/**/*.rs" }, { file = "Cargo.toml" }, { file = "Cargo.lock" }] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "src/**/*.rs" }, + { file = "Cargo.toml" }, + { file = "Cargo.lock" }, +] [build-system] requires = ["maturin>=1.0,<2.0"] diff --git a/rbufrp/src/lib.rs b/rbufrp/src/lib.rs index 90f34be..f15a3e0 100644 --- a/rbufrp/src/lib.rs +++ b/rbufrp/src/lib.rs @@ -1,19 +1,26 @@ -use librbufr::{Decoder, parse}; use pyo3::prelude::*; #[pymodule] mod _core { use librbufr::{ + Decoder, block::{BUFRFile as IB, MessageBlock as IM}, - decoder::BUFRData, + decoder::BUFRParsed as _BUFRParsed, errors::Error, - parse, + get_tables_base_path, parse, set_tables_base_path, }; use pyo3::prelude::*; #[pyfunction] - fn hello_from_bin() -> String { - "Hello from rbufrp!".to_string() + fn set_tables_path(path: &str) -> PyResult<()> { + set_tables_base_path(path); + Ok(()) + } + + #[pyfunction] + fn get_tables_path() -> PyResult { + let path = get_tables_base_path(); + Ok(path.to_string_lossy().to_string()) } #[pyclass] @@ -26,7 +33,7 @@ mod _core { BUFRDecoder {} } - fn decode_bufr(&self, file_path: &str) -> PyResult { + fn decode(&self, file_path: &str) -> PyResult { let parsed = parse(file_path).map_err(|e| match e { Error::Io(io_err) => { PyErr::new::(format!("IO Error: {}", io_err)) @@ -48,6 +55,24 @@ mod _core { Ok(BUFRFile(parsed)) } + + fn parse_message(&self, message: &BUFRMessage) -> PyResult { + self._parse_message(message).map_err(|e| { + PyErr::new::(format!( + "Error parsing BUFR message: {}", + e + )) + }) + } + } + + impl BUFRDecoder { + fn _parse_message(&self, message: &BUFRMessage) -> librbufr::errors::Result { + let _message = &message.message; + let mut decoder = Decoder::from_message(_message)?; + let record = decoder.decode(_message)?.into_owned(); + Ok(BUFRParsed(record)) + } } #[pyclass] @@ -78,4 +103,25 @@ mod _core { struct BUFRMessage { message: IM, } + + #[pymethods] + impl BUFRMessage { + fn __repr__(&self) -> String { + format!("{}", self.message) + } + + fn version(&self) -> u8 { + self.message.version() + } + } + + #[pyclass] + struct BUFRParsed(_BUFRParsed<'static>); + + #[pymethods] + impl BUFRParsed { + fn __repr__(&self) -> String { + format!("{}", &self.0) + } + } } diff --git a/rbufrp/src/rbufrp/__init__.py b/rbufrp/src/rbufrp/__init__.py index 57dbcc7..0deeeae 100644 --- a/rbufrp/src/rbufrp/__init__.py +++ b/rbufrp/src/rbufrp/__init__.py @@ -1,5 +1,93 @@ -from rbufrp._core import hello_from_bin +""" +rbufrp - BUFR (Binary Universal Form for the Representation of meteorological data) decoder +""" + +import os +import sys +from pathlib import Path +from typing import Optional + +# Import the Rust extension module +from ._core import ( + set_tables_path, + get_tables_path, + BUFRDecoder, + BUFRFile, + BUFRMessage, + BUFRParsed, +) + +__version__ = "0.1.0" +__all__ = [ + "BUFRDecoder", + "BUFRFile", + "BUFRMessage", + "BUFRParsed", + "set_tables_path", + "get_tables_path", + "initialize_tables_path", +] + + +def _find_tables_directory() -> Optional[Path]: + env_path = os.environ.get("RBUFR_TABLES_PATH") + if env_path: + tables_path = Path(env_path) + if tables_path.exists() and tables_path.is_dir(): + return tables_path + + package_dir = Path(__file__).parent + installed_tables = package_dir / "tables" + if installed_tables.exists() and installed_tables.is_dir(): + return installed_tables + + dev_tables = package_dir.parent.parent.parent / "rbufr" / "tables" + if dev_tables.exists() and dev_tables.is_dir(): + return dev_tables + + cwd_tables = Path.cwd() / "tables" + if cwd_tables.exists() and cwd_tables.is_dir(): + return cwd_tables + + return None + + +def initialize_tables_path(custom_path: Optional[str | Path] = None) -> None: + if custom_path: + custom_path = Path(custom_path) + if not custom_path.exists(): + raise RuntimeError(f"指定的 tables 路径不存在: {custom_path}") + set_tables_path(str(custom_path.absolute())) + return + + tables_dir = _find_tables_directory() + if tables_dir is None: + raise RuntimeError( + "无法找到 BUFR tables 目录。请执行以下操作之一:\n" + "1. 设置环境变量 RBUFR_TABLES_PATH\n" + "2. 使用 initialize_tables_path('/path/to/tables') 手动指定\n" + "3. 确保在包含 tables 目录的位置运行" + ) + + set_tables_path(str(tables_dir.absolute())) + + +# 自动初始化 tables 路径 +try: + initialize_tables_path() +except RuntimeError as e: + import warnings + warnings.warn( + f"Tables 路径自动初始化失败: {e}\n" + "您可以稍后手动调用 rbufrp.initialize_tables_path() 来设置", + UserWarning + ) def main() -> None: - print(hello_from_bin()) + """命令行入口点""" + print(f"Tables path: {get_tables_path()}") + + +if __name__ == "__main__": + main() diff --git a/rbufrp/src/rbufrp/__pycache__/__init__.cpython-312.pyc b/rbufrp/src/rbufrp/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..361ca44 Binary files /dev/null and b/rbufrp/src/rbufrp/__pycache__/__init__.cpython-312.pyc differ diff --git a/rbufrp/src/rbufrp/_core.abi3.so b/rbufrp/src/rbufrp/_core.abi3.so index bf5a962..7990743 100755 Binary files a/rbufrp/src/rbufrp/_core.abi3.so and b/rbufrp/src/rbufrp/_core.abi3.so differ diff --git a/rbufrp/src/rbufrp/_core.pyi b/rbufrp/src/rbufrp/_core.pyi index d52129e..696cada 100644 --- a/rbufrp/src/rbufrp/_core.pyi +++ b/rbufrp/src/rbufrp/_core.pyi @@ -1 +1,151 @@ -def hello_from_bin() -> str: ... +""" +Type stubs for rbufrp._core + +This file provides type hints for the Rust extension module. +""" + +from typing import Optional + +class BUFRDecoder: + """BUFR decoder for parsing BUFR files.""" + + def __init__(self) -> None: + """Create a new BUFR decoder instance.""" + ... + + def decode(self, file_path: str) -> BUFRFile: + """ + Decode a BUFR file from the given path. + + Args: + file_path: Path to the BUFR file to decode + + Returns: + BUFRFile: Parsed BUFR file containing messages + + Raises: + IOError: If the file cannot be read + ValueError: If the file is not a valid BUFR file + """ + ... + + def parse_message(self, message: BUFRMessage) -> BUFRParsed: + """ + Parse a single BUFR message. + + Args: + message: The BUFR message to parse + + Returns: + BUFRParsed: Parsed data from the message + + Raises: + Exception: If parsing fails + """ + ... + +class BUFRFile: + """ + Represents a parsed BUFR file containing one or more messages. + """ + + def __repr__(self) -> str: + """Return a string representation of the BUFR file.""" + ... + + def message_count(self) -> int: + """ + Get the number of messages in the file. + + Returns: + int: Number of BUFR messages + """ + ... + + def get_message(self, index: int) -> BUFRMessage: + """ + Get a specific message by index. + + Args: + index: Zero-based index of the message + + Returns: + BUFRMessage: The requested message + + Raises: + IndexError: If the index is out of range + """ + ... + +class BUFRMessage: + """ + Represents a single BUFR message. + """ + + def __repr__(self) -> str: + """Return a string representation of the message.""" + ... + + def version(self) -> int: + """ + Get the BUFR edition/version number. + + Returns: + int: BUFR edition (typically 2, 3, or 4) + """ + ... + +class BUFRParsed: + """ + Represents parsed BUFR data. + + This class contains the decoded meteorological data from a BUFR message. + """ + + def __repr__(self) -> str: + """ + Return a formatted string representation of the parsed data. + + Returns: + str: Human-readable representation of all records + """ + ... + +def set_tables_path(path: str) -> None: + """ + Set the base path for BUFR table files. + + This function configures where the decoder should look for BUFR table files + (Table B, Table D, etc.) needed for decoding messages. + + Args: + path: Absolute path to the directory containing BUFR tables + + Example: + >>> import rbufrp + >>> rbufrp.set_tables_path("/usr/share/bufr/tables") + """ + ... + +def get_tables_path() -> str: + """ + Get the currently configured base path for BUFR table files. + + Returns: + str: Current tables directory path + + Example: + >>> import rbufrp + >>> print(rbufrp.get_tables_path()) + /usr/share/bufr/tables + """ + ... + +__all__ = [ + "BUFRDecoder", + "BUFRFile", + "BUFRMessage", + "BUFRParsed", + "set_tables_path", + "get_tables_path", +] diff --git a/rbufrp/src/rbufrp/py.typed b/rbufrp/src/rbufrp/py.typed new file mode 100644 index 0000000..e69de29