From fba2ba640550764bb78fbd660bc6be36318c787d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:25:08 +0000 Subject: [PATCH] feat: Add SOR file writing to Python library This commit extends the Python library to allow writing SOR files. The following methods have been added to the `SORFile` class: - `to_bytes() -> bytes`: Returns the SOR file as a Python bytes object. - `write_file(path: str) -> None`: Writes the SOR file to the specified path. The core serialization logic already existed in the Rust library, and this change exposes it to the Python bindings using `pyo3`. A new test file has been added to verify the round-trip integrity of parsing a file, writing it back to bytes/file, and parsing it again. The tests confirm that all data blocks (except for the re-calculated MapBlock) are preserved correctly. --- otdrs.pyi | 8 ++++ src/python.rs | 29 +++++++++++- ..._python_otdrs.cpython-311-pytest-8.4.1.pyc | Bin 0 -> 13768 bytes tests/test_python_otdrs.py | 43 ++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/__pycache__/test_python_otdrs.cpython-311-pytest-8.4.1.pyc create mode 100644 tests/test_python_otdrs.py diff --git a/otdrs.pyi b/otdrs.pyi index 144cef1..81026e8 100644 --- a/otdrs.pyi +++ b/otdrs.pyi @@ -272,6 +272,14 @@ class SORFile: data_points: DataPoints | None proprietary_blocks: list[ProprietaryBlock] + def to_bytes(self) -> bytes: + """Returns the SOR file as a byte string.""" + ... + + def write_file(self, path: str) -> None: + """Writes the SOR file to the given path.""" + ... + def parse_file(path: str) -> SORFile: """Load a SOR from the given path and parse it""" diff --git a/src/python.rs b/src/python.rs index f5629f6..0d3623a 100644 --- a/src/python.rs +++ b/src/python.rs @@ -4,7 +4,8 @@ use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use pyo3::types::PyBytes; use std::fs::File; -use std::io::Read; +use std::io::{Read, Write}; + /// Loads an OTDR file and returns the result #[pyfunction] fn parse_file(path: String) -> PyResult { @@ -30,10 +31,36 @@ fn parse_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult { return result; } +#[pymethods] +impl SORFile { + /// Returns the SOR file as a byte string. + #[pyo3(name = "to_bytes")] + fn to_bytes_py<'py>(&self, py: Python<'py>) -> PyResult> { + match self.to_bytes() { + Ok(bytes) => Ok(PyBytes::new(py, &bytes)), + Err(err) => Err(PyRuntimeError::new_err(err.to_string())), + } + } + + /// Writes the SOR file to the given path. + #[pyo3(name = "write_file")] + fn write_file_py(&self, path: String) -> PyResult<()> { + match self.to_bytes() { + Ok(bytes) => { + let mut file = std::fs::File::create(path)?; + file.write_all(&bytes)?; + Ok(()) + } + Err(err) => Err(PyRuntimeError::new_err(err.to_string())), + } + } +} + /// This module is implemented in Rust. #[pymodule] fn otdrs(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(parse_file, m)?)?; m.add_function(wrap_pyfunction!(parse_bytes, m)?)?; + m.add_class::()?; return Ok(()); } diff --git a/tests/__pycache__/test_python_otdrs.cpython-311-pytest-8.4.1.pyc b/tests/__pycache__/test_python_otdrs.cpython-311-pytest-8.4.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67cc27201ced985a362c73563aa488c5fbe757df GIT binary patch literal 13768 zcmeHN&2QV-5vL?drX)LZV(-R5KO#F#9DhXiI@=@#)`qj#>^g>{2FW5w4c7u~(urb8 zR7pCC9SLxY9CB!Z-dYqr_>{Ux4}0t%&_93@D6Bys#R9#ww*ct^L3-)D_sHQRlXzFy zONB~)e)`SKZ{`h$L&}fVpZfYz0=)kEx8L9WMUNo-g9zR$>VlVZ0DdPZf)XhS%Mtn> zEyvcQ5uq$D$0N*=SWZw&&vFlamzE_Z`dC;_Dv_JQSnOXUSbp6NN#9^SAqKxM|NR3J z-;DUidx(|DQ^3zqt_w9`&&$kr0Q3}opP{B63B)7o&{r)SFIp4siJr9{t3^3Uti@{L zCS9!_uf>gjnrV+I2=@{mR-(1|Mns7b_D1w6M0``pj4`q7wg(8AgNg-kD*D2N}lSk)arjs3iU$+Yx>Qe^6CMZn?|Zl= z1=$fHNp`s0&c}-?_I5@nDa8EU+{6 zcj5mFd)ySa<8ANW)v0^jZXbBhI{aO2td_dxOYQYRr|xmPec<(hzpEvE;7jfGL8tC< zyM5sGfxoLIec(&&^+BiZal3us^?|>uC4Jya?e#&Y?s2<);PrvOt0jHlOYQYRr|xmP zeL(vluxB0ecB0|^CHx0Cc~dwgd|UPd;bH7&!h^`zz>b;9!s~O@FpaEvS2nY9_U2D- zWH*W>HM=fXvTJHqS7l`~`#>+6rdr9GT6S&QRE^23Tv4bkyC$!1X5~9_u|k7AD4KV* zEi=1*SFYSCR_;Jth^1A)+sskM&JPvYl&92(a=BVkKR8#>wpHVtwo#h7G@UaveJt`i zIy*ZS*;zY%wz@qtW~3guAUm5SXnM@Z-BBy5E|&^bS(nSIsp>|mo}Fd(3!V!A*X6@^ zxH!F>DpFBG4*1Vh-0D?gVo41L#7YObYbU+Y#6F;(o`;u8AN$059un0 zq-$(dtEHlQGZo!%QqcK^N|laeY6W^(I%%e5!m&3m5+^lZCD*T6 zkag#1VO=hj3c6a=*R?Wus!lJM3u{}&l3A=6P6CcdE*Z{H!GLSJq)=+%PDxvX>!kwM zos%pW@`g$xI7cqi%c7`NuIM^kO9O=sO)tx4L48;)$rafIi!<<+rgr+nP3WZj)^ReS zbM2(L6XYCwvz$;4wOl3Da(c>}1`(X7W;pTnl4ht*irmZG!Xfm7CxK4}bG zu?Mc~E`8Pi{V)4ZH~UXF`bX{l(cP=hfiZY`uh z0o?FRjDsS2k$Sk^jKV391WSfkC?aRSjye{IQy>WvM;0gn!^{t+fdDK9WvM;0gn!^{t+fdDK9an7cISlOT)zzxsDI4GhQsfX*$D4YUGuw;mZB69ZN5(@<46i9-^kp)V?F!O_H zAQL<$#^=3wK44m)QA=FdpS33z;N41(S`!P+G{qo_3oUWMrhr)4pd7#r&%`(=q8F)$ z>&+;f0!grBh=n3@_GeMY0&xl?LE^{)C19BO!88zn#b6vbINpor1EvK!Wr_29b$fIk z-mUa0YjnPurWizVz9r7v6c8&LlmoconHUE}^dj|ey%~j5APJTXu~0e8-kBRYlFP;yW7HAyq*4!)mVKQNP7K>aXTL~TLl+xE7j2lu z_jW&i()V0UJYINm10znx!2c_a<-JSq}=n7QF~YJ46#u3IWt#L=_bT!IGUdmD8U70elQJ0aD#ZqfWdh(!v~~BQ-ULeb}(Uqq#w5U1g2eomkS z7nu3MG!VfJ;vEA9=gADy8ckZ#ZEN~7d-^s3nMrHD z(oKlda5O(BP=X80{9qc0;0E!I0fX~ohG~r^Ea{dt^{G8|i+~LLoqnsCp%_HzR!h2N zQ$Vb2P!8aRXJQ-_(TkMC^=1@Kfh1Tm#6r>MOnr(DQ{CitZYyY;D%>n92C)ul*ILB6i$I8 zSTe*y(dSHjj7m2lPQ%gsoInXKF!O_HAc7mjI|dBSlNqKp%8~nb_j4`$;q84O!XFdK z^ske@J=c)3wv@HJ@#;6xu=drfR|a|7@%O~fguOeXR)QjGHjMh(#ii1M