Skip to content

Commit

Permalink
feat: made it optional to whether convert_ilcd should return string, …
Browse files Browse the repository at this point in the history
…dict or pydantic
  • Loading branch information
ocni-dtu committed Jan 23, 2024
1 parent 5da9fca commit 720ca62
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ docs/schemas
.vscode
.idea
/site/
/.venv/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ cargo test --package epdx --target x86_64-unknown-linux-gnu

```bash
maturin develop --extras tests --target x86_64-unknown-linux-gnu
source .venv/bin/active .
cd packages/python
pytest tests/
```
Expand Down
5 changes: 0 additions & 5 deletions packages/python/epdx/__init__.py

This file was deleted.

34 changes: 34 additions & 0 deletions packages/python/src/epdx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import json

from .epdx import *
from .pydantic import *

__doc__ = epdx.__doc__
if hasattr(epdx, "__all__"):
__all__ = epdx.__all__


def convert_ilcd(data: str, *, as_type: str = "dict"):
"""
Converts a json formatted ILCD+EPD data into EPDx
The EPDx data can either be returned as a string, a dict or a Pydantic class.
"""

try:
_epd = epdx._convert_ilcd(data)
except Exception as err:
raise ParsingException(err)

if as_type == "str":
return _epd
elif as_type == "dict":
return json.loads(_epd)
elif as_type == "pydantic":
return EPD(**json.loads(_epd))
else:
raise NotImplemented("Currently only 'dict', 'str' and 'pydantic' is implemented as_type.")


class ParsingException(Exception):
...
Binary file added packages/python/src/epdx/epdx.abi3.so
Binary file not shown.
3 changes: 3 additions & 0 deletions packages/python/src/epdx/epdx.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

def _convert_ilcd(data: str) -> str:
"""Converts a json formatted ILCD+EPD data string into an EPDx formatted json string"""
101 changes: 101 additions & 0 deletions packages/python/src/epdx/pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# generated by datamodel-codegen:
# filename: epdx.schema.json
# timestamp: 2024-01-23T14:25:59+00:00

from __future__ import annotations

from datetime import datetime
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, conint


class ImpactCategory(BaseModel):
a1a3: Optional[float] = None
a4: Optional[float] = None
a5: Optional[float] = None
b1: Optional[float] = None
b2: Optional[float] = None
b3: Optional[float] = None
b4: Optional[float] = None
b5: Optional[float] = None
b6: Optional[float] = None
b7: Optional[float] = None
c1: Optional[float] = None
c2: Optional[float] = None
c3: Optional[float] = None
c4: Optional[float] = None
d: Optional[float] = None


class Standard(Enum):
EN15804A1 = 'EN15804A1'
EN15804A2 = 'EN15804A2'
UNKNOWN = 'UNKNOWN'


class SubType(Enum):
Generic = 'Generic'
Specific = 'Specific'
Industry = 'Industry'
Representative = 'Representative'


class Unit(Enum):
M = 'M'
M2 = 'M2'
M3 = 'M3'
KG = 'KG'
TONES = 'TONES'
PCS = 'PCS'
L = 'L'
M2R1 = 'M2R1'
UNKNOWN = 'UNKNOWN'


class Conversion(BaseModel):
to: Unit
value: float


class EPD(BaseModel):
adpe: Optional[ImpactCategory] = None
adpf: Optional[ImpactCategory] = None
ap: Optional[ImpactCategory] = None
comment: Optional[str] = None
conversions: Optional[List[Conversion]] = None
cru: Optional[ImpactCategory] = None
declared_unit: Unit
eee: Optional[ImpactCategory] = None
eet: Optional[ImpactCategory] = None
ep: Optional[ImpactCategory] = None
format_version: str
fw: Optional[ImpactCategory] = None
gwp: Optional[ImpactCategory] = None
hwd: Optional[ImpactCategory] = None
id: str
location: str
mer: Optional[ImpactCategory] = None
mrf: Optional[ImpactCategory] = None
name: str
nhwd: Optional[ImpactCategory] = None
nrsf: Optional[ImpactCategory] = None
odp: Optional[ImpactCategory] = None
penre: Optional[ImpactCategory] = None
penrm: Optional[ImpactCategory] = None
penrt: Optional[ImpactCategory] = None
pere: Optional[ImpactCategory] = None
perm: Optional[ImpactCategory] = None
pert: Optional[ImpactCategory] = None
pocp: Optional[ImpactCategory] = None
published_date: datetime
reference_service_life: Optional[conint(ge=0)] = None
rsf: Optional[ImpactCategory] = None
rwd: Optional[ImpactCategory] = None
sm: Optional[ImpactCategory] = None
source: Optional[str] = None
standard: Standard
subtype: SubType
valid_until: datetime
version: str
21 changes: 21 additions & 0 deletions packages/python/tests/test_parse.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

import epdx


Expand All @@ -6,3 +8,22 @@ def test_parse_ilcd(datafix_dir):
epd = epdx.convert_ilcd(ilcd_file.read_text())

assert isinstance(epd, dict)


def test_parse_empty():
with pytest.raises(epdx.ParsingException):
epdx.convert_ilcd("{}")


def test_parse_ilcd_str(datafix_dir):
ilcd_file = datafix_dir / "f63ac879-fa7d-4f91-813e-e816cbdf1927.json"
epd = epdx.convert_ilcd(ilcd_file.read_text(), as_type='str')

assert isinstance(epd, str)


def test_parse_ilcd_pydantic(datafix_dir):
ilcd_file = datafix_dir / "f63ac879-fa7d-4f91-813e-e816cbdf1927.json"
epd = epdx.convert_ilcd(ilcd_file.read_text(), as_type='pydantic')

assert isinstance(epd, epdx.EPD)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ build-backend = "maturin"
[tool.maturin]
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
features = ["pybindings"]
python-source = "packages/python"
python-source = "packages/python/src"
40 changes: 7 additions & 33 deletions src/python.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
use pyo3::prelude::*;
use pyo3::types::PyDict;
use crate::epd::EPD;
use pyo3::exceptions::PyTypeError;
use crate::parse;

// #[cfg(feature = "pybindings")]
// #[pyfunction]
// fn make_dict<'py>(py: Python<'py>, epd: &EPD) -> PyResult<&'py PyDict> {
// let dict = PyDict::new(py);
// for value in epd.iter() {
// dict.set_item(value.name, value.value).unwrap();
// }
// Ok(dict)
// }

// #[pyclass]
// struct EPDWrapper(EPD);
//
// impl IntoPy<Py<PyDict>> for EPDWrapper {
// // https://stackoverflow.com/questions/71676419/iterate-over-struct-in-rust
// fn into_py(self, py: Python<'_>) -> Py<PyDict> {
// let mut dict = PyDict::new(py).into_py(py);
// let epd = self.0;
// for value in epd.iter() {
// dict.set_item(value.name, value.value).unwrap();
// }
// dict
// }
// }

#[cfg(feature = "pybindings")]
#[pyfunction]
pub fn convert_ilcd(json: String) -> String {
pub fn _convert_ilcd(json: String) -> PyResult<String> {
let epd = parse::parse_ilcd(json);
serde_json::to_string(&epd).unwrap()
// Python::with_gil(|py| {
// EPDWrapper(epd).into_py(py)
// })
match epd {
Ok(epd) => Ok(serde_json::to_string(&epd).unwrap()),
Err(error) => Err(PyTypeError::new_err(error.to_string()))
}
}

/// A Python module implemented in Rust. The name of this function must match
Expand All @@ -44,7 +18,7 @@ pub fn convert_ilcd(json: String) -> String {
#[cfg(feature = "pybindings")]
#[pymodule]
fn epdx(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(convert_ilcd, m)?)?;
m.add_function(wrap_pyfunction!(_convert_ilcd, m)?)?;

Ok(())
}

0 comments on commit 720ca62

Please sign in to comment.