Repository navigation
feat: expose workbook document properties #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5587829
e13973e
fac7c62
9203b48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| use std::collections::BTreeMap; | ||
|
|
||
| use chrono::{DateTime, FixedOffset, NaiveDateTime}; | ||
| use pyo3::prelude::*; | ||
|
|
||
| /// A document property date, with a UTC offset if the file stores one. | ||
| #[derive(Clone, Debug, PartialEq, IntoPyObject)] | ||
| enum PropertyDate { | ||
| Aware(DateTime<FixedOffset>), | ||
| Naive(NaiveDateTime), | ||
| } | ||
|
|
||
| impl PropertyDate { | ||
| /// Parse a date such as "2024-01-15T08:30:00Z" (xlsx, xlsb, xls) or | ||
| /// "2024-06-20T14:22:00.123456789" (ods). Other formats return `None`. | ||
| fn parse(value: &str) -> Option<Self> { | ||
| let value = value.trim(); | ||
| if let Ok(date) = DateTime::parse_from_rfc3339(value) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject dates outside Python's supported year range. For When Python reads
|
||
| return Some(PropertyDate::Aware(date)); | ||
| } | ||
| NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f") | ||
| .ok() | ||
| .map(PropertyDate::Naive) | ||
| } | ||
| } | ||
|
|
||
| /// Document properties of a workbook, such as the creator, title and company. | ||
| #[pyclass(eq, from_py_object)] | ||
| #[derive(Clone, Debug, Default, PartialEq)] | ||
| pub struct WorkbookProperties { | ||
| #[pyo3(get)] | ||
| creator: Option<String>, | ||
| #[pyo3(get)] | ||
| last_modified_by: Option<String>, | ||
| #[pyo3(get)] | ||
| created: Option<PropertyDate>, | ||
| #[pyo3(get)] | ||
| modified: Option<PropertyDate>, | ||
| #[pyo3(get)] | ||
| title: Option<String>, | ||
| #[pyo3(get)] | ||
| subject: Option<String>, | ||
| #[pyo3(get)] | ||
| description: Option<String>, | ||
| #[pyo3(get)] | ||
| keywords: Option<String>, | ||
| #[pyo3(get)] | ||
| category: Option<String>, | ||
| #[pyo3(get)] | ||
| content_status: Option<String>, | ||
| #[pyo3(get)] | ||
| revision: Option<String>, | ||
| #[pyo3(get)] | ||
| version: Option<String>, | ||
| #[pyo3(get)] | ||
| application: Option<String>, | ||
| #[pyo3(get)] | ||
| app_version: Option<String>, | ||
| #[pyo3(get)] | ||
| company: Option<String>, | ||
| #[pyo3(get)] | ||
| template: Option<String>, | ||
| #[pyo3(get)] | ||
| manager: Option<String>, | ||
| #[pyo3(get)] | ||
| custom_properties: BTreeMap<String, String>, | ||
| } | ||
|
|
||
| #[pymethods] | ||
| impl WorkbookProperties { | ||
| fn __repr__(&self, py: Python<'_>) -> PyResult<String> { | ||
| let fields = [ | ||
| ("creator", self.creator.clone().into_pyobject(py)?), | ||
| ( | ||
| "last_modified_by", | ||
| self.last_modified_by.clone().into_pyobject(py)?, | ||
| ), | ||
| ("created", self.created.clone().into_pyobject(py)?), | ||
| ("modified", self.modified.clone().into_pyobject(py)?), | ||
| ("title", self.title.clone().into_pyobject(py)?), | ||
| ("subject", self.subject.clone().into_pyobject(py)?), | ||
| ("description", self.description.clone().into_pyobject(py)?), | ||
| ("keywords", self.keywords.clone().into_pyobject(py)?), | ||
| ("category", self.category.clone().into_pyobject(py)?), | ||
| ( | ||
| "content_status", | ||
| self.content_status.clone().into_pyobject(py)?, | ||
| ), | ||
| ("revision", self.revision.clone().into_pyobject(py)?), | ||
| ("version", self.version.clone().into_pyobject(py)?), | ||
| ("application", self.application.clone().into_pyobject(py)?), | ||
| ("app_version", self.app_version.clone().into_pyobject(py)?), | ||
| ("company", self.company.clone().into_pyobject(py)?), | ||
| ("template", self.template.clone().into_pyobject(py)?), | ||
| ("manager", self.manager.clone().into_pyobject(py)?), | ||
| ]; | ||
| let mut parts = Vec::new(); | ||
| for (name, value) in fields { | ||
| if !value.is_none() { | ||
| parts.push(format!("{name}={}", value.repr()?)); | ||
| } | ||
| } | ||
| if !self.custom_properties.is_empty() { | ||
| let custom = (&self.custom_properties).into_pyobject(py)?; | ||
| parts.push(format!("custom_properties={}", custom.repr()?)); | ||
| } | ||
| Ok(format!("WorkbookProperties({})", parts.join(", "))) | ||
| } | ||
| } | ||
|
|
||
| impl From<calamine::WorkbookProperties> for WorkbookProperties { | ||
| fn from(props: calamine::WorkbookProperties) -> Self { | ||
| WorkbookProperties { | ||
| creator: props.creator, | ||
| last_modified_by: props.last_modified_by, | ||
| created: props.created.as_deref().and_then(PropertyDate::parse), | ||
| modified: props.modified.as_deref().and_then(PropertyDate::parse), | ||
| title: props.title, | ||
| subject: props.subject, | ||
| description: props.description, | ||
| keywords: props.keywords, | ||
| category: props.category, | ||
| content_status: props.content_status, | ||
| revision: props.revision, | ||
| version: props.version, | ||
| application: props.application, | ||
| app_version: props.app_version, | ||
| company: props.company, | ||
| template: props.template, | ||
| manager: props.manager, | ||
| custom_properties: props.custom_properties.into_iter().collect(), | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declare the metadata fields as read-only properties.
The stub permits
props.creator = "Replacement". The Rust class exposes only#[pyo3(get)], so this assignment raisesAttributeErrorat runtime. PyO3 defines get-only fields as read-only. (raw.githubusercontent.com)Declare getter-only
@propertymethods for all 18 fields, includingcustom_properties. Keep the returned mapping type asdict[str, str].