sync
Some checks failed
CI / Rust Tests (push) Waiting to run
CI / Python Tests (macos-latest, 3.11) (push) Waiting to run
CI / Python Tests (macos-latest, 3.13) (push) Waiting to run
CI / Python Tests (macos-latest, 3.8) (push) Waiting to run
CI / Python Tests (ubuntu-latest, 3.11) (push) Waiting to run
CI / Python Tests (ubuntu-latest, 3.13) (push) Waiting to run
CI / Python Tests (ubuntu-latest, 3.8) (push) Waiting to run
CI / Python Tests (windows-latest, 3.11) (push) Waiting to run
CI / Python Tests (windows-latest, 3.13) (push) Waiting to run
CI / Python Tests (windows-latest, 3.8) (push) Waiting to run
CI / Check Formatting (push) Waiting to run
Build Wheels (cibuildwheel) / Build wheels on Linux (aarch64) (push) Has been cancelled
Build Wheels (cibuildwheel) / Build wheels on Linux (x86_64) (push) Has been cancelled
Build Wheels (cibuildwheel) / Build wheels on macOS (arm64) (push) Has been cancelled
Build Wheels (cibuildwheel) / Build wheels on macOS (x86_64) (push) Has been cancelled
Build Wheels (cibuildwheel) / Build wheels on Windows (push) Has been cancelled
Build Wheels (cibuildwheel) / Build source distribution (push) Has been cancelled
Build Wheels (cibuildwheel) / Publish to GitHub Release (push) Has been cancelled

This commit is contained in:
Tsuki 2026-01-08 01:27:20 +08:00
parent 2ae2d579b8
commit d3f020b6c3
17 changed files with 971 additions and 107 deletions

View File

@ -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

103
.github/workflows/ci.yml vendored Normal file
View File

@ -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/

1
BUFR4 Submodule

@ -0,0 +1 @@
Subproject commit a6b7ab078d4c70c69565655f7cf7c7d3913d6d78

View File

@ -21,6 +21,7 @@ rustc-hash = "2.1.1"
[features] [features]
default = ["opera"] default = ["opera"]
opera = ["gentools/opera"] opera = ["gentools/opera"]
python_bindings = []
[profile.bench] [profile.bench]

View File

@ -11,7 +11,11 @@ use genlib::{
prelude::{BUFRTableB, BUFRTableBitMap, BUFRTableD}, prelude::{BUFRTableB, BUFRTableBitMap, BUFRTableD},
tables::{ArchivedBTableEntry, ArchivedDTableEntry}, tables::{ArchivedBTableEntry, ArchivedDTableEntry},
}; };
use std::{fmt::Display, ops::Deref}; use std::{
borrow::{Borrow, Cow},
fmt::Display,
ops::Deref,
};
const MISS_VAL: f64 = 99999.999999; 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> { pub struct BUFRParsed<'a> {
records: Vec<BUFRRecord<'a>>, records: Vec<BUFRRecord<'a>>,
} }
@ -1385,11 +1390,11 @@ impl<'a> BUFRParsed<'a> {
Self { records: vec![] } 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 { self.records.push(BUFRRecord {
name: Some(element_name), name: Some(Cow::Borrowed(element_name)),
values: BUFRData::Single(value), 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), 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> { struct Array<'a, 's> {
@ -1424,9 +1435,9 @@ impl<'a> Array<'a, '_> {
fn finish(self, name: Option<&'a str>, unit: Option<&'a str>) { fn finish(self, name: Option<&'a str>, unit: Option<&'a str>) {
let recording = BUFRRecord { let recording = BUFRRecord {
name, name: name.map(|n| Cow::Borrowed(n)),
values: BUFRData::Array(self.values), values: BUFRData::Array(self.values),
unit, unit: unit.map(|u| Cow::Borrowed(u)),
}; };
self.parsed.records.push(recording); self.parsed.records.push(recording);
} }
@ -1452,21 +1463,38 @@ impl<'a, 's> Repeating<'a, 's> {
} }
} }
#[derive(Debug, Clone)]
pub enum BUFRData { pub enum BUFRData {
Repeat(Vec<Value>), Repeat(Vec<Value>),
Single(Value), Single(Value),
Array(Vec<f64>), Array(Vec<f64>),
} }
#[derive(Clone)]
pub struct BUFRRecord<'a> { pub struct BUFRRecord<'a> {
pub name: Option<&'a str>, // pub name: Option<&'a str>,
pub name: Option<Cow<'a, str>>,
pub values: BUFRData, pub values: BUFRData,
pub unit: Option<&'a str>, pub unit: Option<Cow<'a, str>>,
}
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<'_> { impl Display for BUFRRecord<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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, Some("CAITT IA5" | "code table" | "code-table" | "flag table" | "flag-table") => false,
None => false, None => false,
_ => true, _ => true,
@ -1477,79 +1505,33 @@ impl Display for BUFRRecord<'_> {
} }
let name = self.name.as_ref().unwrap(); let name = self.name.as_ref().unwrap();
let width = f.width().unwrap_or(0);
match &self.values { match &self.values {
BUFRData::Single(v) => { BUFRData::Single(v) => {
if is_print_unit { if width > 0 {
write!(f, "{}: {} {}", name, v, self.unit.as_ref().unwrap())?; write!(f, "{:<width$} : ", name, width = width)?;
} else { } else {
write!(f, "{}: {}", name, v)?; write!(f, "{} : ", name)?;
}
match v {
Value::Missing => 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) => { BUFRData::Repeat(vs) => {
if vs.len() < 8 { self.format_sequence(f, name, vs, is_print_unit, width)?;
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, "]")?;
}
} }
BUFRData::Array(a) => { BUFRData::Array(a) => {
if a.len() < 8 { self.format_array(f, name, a, is_print_unit, width)?;
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, "]")?;
}
} }
} }
@ -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, "{:<width$} : ", name, width = width)?;
} else {
write!(f, "{} : ", name)?;
}
write!(f, "[len={}", values.len())?;
if missing_count > 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<f64> = values.iter().copied().filter(|&v| v != MISS_VAL).collect();
if width > 0 {
write!(f, "{:<width$} : ", name, width = width)?;
} else {
write!(f, "{} : ", name)?;
}
write!(f, "[len={}", values.len())?;
if missing_count > 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::<f64>() / 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<'_> { impl Display for BUFRParsed<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 { for record in &self.records {
writeln!(f, "{:<max_name_len$}", record, max_name_len = max_name_len)?;
}
Ok(())
}
}
impl BUFRParsed<'_> {
/// 获取记录数量
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)?; writeln!(f, "{}", record)?;
} }
Ok(()) 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 {}: {:<max_name_len$}", idx + 1, record, max_name_len = max_name_len)?;
}
Ok(())
}
}
enum Frame<'v, 'a> { enum Frame<'v, 'a> {
Slice { Slice {
descs: Descs<'v>, descs: Descs<'v>,

View File

@ -5,7 +5,9 @@ pub mod errors;
pub mod opera; pub mod opera;
pub mod parser; pub mod parser;
pub mod structs; pub mod structs;
pub mod table_path;
pub mod tables; pub mod tables;
pub use crate::decoder::Decoder; pub use crate::decoder::Decoder;
pub use crate::parser::*; pub use crate::parser::*;
pub use crate::table_path::{get_tables_base_path, set_tables_base_path};

65
rbufr/src/table_path.rs Normal file
View File

@ -0,0 +1,65 @@
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static TABLES_BASE_PATH: OnceLock<PathBuf> = OnceLock::new();
pub fn set_tables_base_path<P: AsRef<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<PathBuf> {
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<P: AsRef<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")
);
}
}

View File

@ -52,18 +52,16 @@ impl LocalTable {
} }
impl TableTrait for MasterTable { impl TableTrait for MasterTable {
fn file_path(&self, table_type: TableType) -> PathBuf { fn file_path(&self, table_type: TableType) -> PathBuf {
use crate::table_path::get_table_path;
match table_type { match table_type {
TableType::B => { TableType::B => {
let mut base_dir = PathBuf::new(); let file_name = format!("master/BUFR_TableB_{}.bufrtbl", self.version);
base_dir.push("tables/master"); get_table_path(file_name)
let file_name = format!("BUFR_TableB_{}.bufrtbl", self.version);
base_dir.join(file_name)
} }
TableType::D => { TableType::D => {
let mut base_dir = PathBuf::new(); let file_name = format!("master/BUFR_TableD_{}.bufrtbl", self.version);
base_dir.push("tables/master"); get_table_path(file_name)
let file_name = format!("BUFR_TableD_{}.bufrtbl", self.version);
base_dir.join(file_name)
} }
_ => { _ => {
unreachable!("Table type not supported for MasterTable") unreachable!("Table type not supported for MasterTable")
@ -74,26 +72,24 @@ impl TableTrait for MasterTable {
impl TableTrait for LocalTable { impl TableTrait for LocalTable {
fn file_path(&self, table_type: TableType) -> PathBuf { fn file_path(&self, table_type: TableType) -> PathBuf {
use crate::table_path::get_table_path;
match table_type { match table_type {
TableType::B => { TableType::B => {
let mut base_dir = PathBuf::new();
base_dir.push("tables/local");
let sub_center_str = match self.sub_center { let sub_center_str = match self.sub_center {
Some(sc) => format!("{}", sc), Some(sc) => format!("{}", sc),
None => "0".to_string(), None => "0".to_string(),
}; };
let file_name = format!("BUFR_TableB_{}_{}.bufrtbl", sub_center_str, self.version); let file_name = format!("local/BUFR_TableB_{}_{}.bufrtbl", sub_center_str, self.version);
base_dir.join(file_name) get_table_path(file_name)
} }
TableType::D => { TableType::D => {
let mut base_dir = PathBuf::new();
base_dir.push("tables/local");
let sub_center_str = match self.sub_center { let sub_center_str = match self.sub_center {
Some(sc) => format!("{}", sc), Some(sc) => format!("{}", sc),
None => "0".to_string(), None => "0".to_string(),
}; };
let file_name = format!("BUFR_TableD_{}_{}.bufrtbl", sub_center_str, self.version); let file_name = format!("local/BUFR_TableD_{}_{}.bufrtbl", sub_center_str, self.version);
base_dir.join(file_name) get_table_path(file_name)
} }
_ => { _ => {
unreachable!("Table type not supported for LocalTable") unreachable!("Table type not supported for LocalTable")
@ -104,12 +100,12 @@ impl TableTrait for LocalTable {
impl TableTrait for BitmapTable { impl TableTrait for BitmapTable {
fn file_path(&self, table_type: TableType) -> PathBuf { fn file_path(&self, table_type: TableType) -> PathBuf {
use crate::table_path::get_table_path;
match table_type { match table_type {
TableType::BitMap => { TableType::BitMap => {
let mut base_dir = PathBuf::new(); let file_name = format!("opera/BUFR_Opera_Bitmap_{}.bufrtbl", self.center);
base_dir.push("tables/opera"); get_table_path(file_name)
let file_name = format!("BUFR_Opera_Bitmap_{}.bufrtbl", self.center);
base_dir.join(file_name)
} }
_ => { _ => {
unreachable!("Table type not supported for BitmapTable") unreachable!("Table type not supported for BitmapTable")

View File

@ -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 # "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"] } pyo3 = { version = "0.27.1", features = ["extension-module", "abi3-py38"] }
rbufr = { path = "../rbufr" } rbufr = { path = "../rbufr", features = ["python_bindings"] }

1
rbufrp/MANIFEST.in Normal file
View File

@ -0,0 +1 @@
recursive-include ../rbufr/tables *.bufrtbl

View File

@ -3,9 +3,7 @@ name = "rbufrp"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
authors = [ authors = [{ name = "Tsuki", email = "qwin7989@gmail.com" }]
{ name = "Tsuki", email = "qwin7989@gmail.com" }
]
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [] dependencies = []
@ -16,9 +14,18 @@ rbufrp = "rbufrp:main"
module-name = "rbufrp._core" module-name = "rbufrp._core"
python-packages = ["rbufrp"] python-packages = ["rbufrp"]
python-source = "src" python-source = "src"
include = [
{ path = "../rbufr/tables", format = "sdist" },
{ path = "../rbufr/tables", format = "wheel" },
]
[tool.uv] [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] [build-system]
requires = ["maturin>=1.0,<2.0"] requires = ["maturin>=1.0,<2.0"]

View File

@ -1,19 +1,26 @@
use librbufr::{Decoder, parse};
use pyo3::prelude::*; use pyo3::prelude::*;
#[pymodule] #[pymodule]
mod _core { mod _core {
use librbufr::{ use librbufr::{
Decoder,
block::{BUFRFile as IB, MessageBlock as IM}, block::{BUFRFile as IB, MessageBlock as IM},
decoder::BUFRData, decoder::BUFRParsed as _BUFRParsed,
errors::Error, errors::Error,
parse, get_tables_base_path, parse, set_tables_base_path,
}; };
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyfunction] #[pyfunction]
fn hello_from_bin() -> String { fn set_tables_path(path: &str) -> PyResult<()> {
"Hello from rbufrp!".to_string() set_tables_base_path(path);
Ok(())
}
#[pyfunction]
fn get_tables_path() -> PyResult<String> {
let path = get_tables_base_path();
Ok(path.to_string_lossy().to_string())
} }
#[pyclass] #[pyclass]
@ -26,7 +33,7 @@ mod _core {
BUFRDecoder {} BUFRDecoder {}
} }
fn decode_bufr(&self, file_path: &str) -> PyResult<BUFRFile> { fn decode(&self, file_path: &str) -> PyResult<BUFRFile> {
let parsed = parse(file_path).map_err(|e| match e { let parsed = parse(file_path).map_err(|e| match e {
Error::Io(io_err) => { Error::Io(io_err) => {
PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("IO Error: {}", io_err)) PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("IO Error: {}", io_err))
@ -48,6 +55,24 @@ mod _core {
Ok(BUFRFile(parsed)) Ok(BUFRFile(parsed))
} }
fn parse_message(&self, message: &BUFRMessage) -> PyResult<BUFRParsed> {
self._parse_message(message).map_err(|e| {
PyErr::new::<pyo3::exceptions::PyException, _>(format!(
"Error parsing BUFR message: {}",
e
))
})
}
}
impl BUFRDecoder {
fn _parse_message(&self, message: &BUFRMessage) -> librbufr::errors::Result<BUFRParsed> {
let _message = &message.message;
let mut decoder = Decoder::from_message(_message)?;
let record = decoder.decode(_message)?.into_owned();
Ok(BUFRParsed(record))
}
} }
#[pyclass] #[pyclass]
@ -78,4 +103,25 @@ mod _core {
struct BUFRMessage { struct BUFRMessage {
message: IM, 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)
}
}
} }

View File

@ -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: def main() -> None:
print(hello_from_bin()) """命令行入口点"""
print(f"Tables path: {get_tables_path()}")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

View File

@ -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",
]

View File