Repository: ZeroTwentyFifty/pact_methodology

Issue:
Expand CarbonFootprint object data model.
The `CarbonFootprint` class does not implement a full python data model, making interactions with the instance a bit of an annoyance, naturally there are automatically defined dunder methods but a more explicit approach is preferable.

This tickets does not need to implement every dunder method in the object model, but the core/usual suspects would be preferable even if it's just to set the tone and standard for future developments.

A list of what would be desirable is:

- [ ] dict
- [ ] str
- [ ] repr

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty) Report
All modified and coverable lines are covered by tests :white_check_mark:

| [Files with missing lines](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty) | Coverage Δ | |
|---|---|---|
| [pact\_methodology/assurance/assurance.py](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?src=pr&el=tree&filepath=pact_methodology%2Fassurance%2Fassurance.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty#diff-cGFjdF9tZXRob2RvbG9neS9hc3N1cmFuY2UvYXNzdXJhbmNlLnB5) | `98.38% <100.00%> (+0.23%)` | :arrow_up: |
| [...t\_methodology/carbon\_footprint/carbon\_footprint.py](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?src=pr&el=tree&filepath=pact_methodology%2Fcarbon_footprint%2Fcarbon_footprint.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty#diff-cGFjdF9tZXRob2RvbG9neS9jYXJib25fZm9vdHByaW50L2NhcmJvbl9mb290cHJpbnQucHk=) | `98.99% <100.00%> (+0.02%)` | :arrow_up: |
| [...arbon\_footprint/product\_or\_sector\_specific\_rule.py](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?src=pr&el=tree&filepath=pact_methodology%2Fcarbon_footprint%2Fproduct_or_sector_specific_rule.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty#diff-cGFjdF9tZXRob2RvbG9neS9jYXJib25fZm9vdHByaW50L3Byb2R1Y3Rfb3Jfc2VjdG9yX3NwZWNpZmljX3J1bGUucHk=) | `97.56% <ø> (ø)` | |
| [tests/assurance/test\_assurance.py](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?src=pr&el=tree&filepath=tests%2Fassurance%2Ftest_assurance.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty#diff-dGVzdHMvYXNzdXJhbmNlL3Rlc3RfYXNzdXJhbmNlLnB5) | `100.00% <100.00%> (ø)` | |
| [tests/carbon\_footprint/test\_carbon\_footprint.py](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?src=pr&el=tree&filepath=tests%2Fcarbon_footprint%2Ftest_carbon_footprint.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty#diff-dGVzdHMvY2FyYm9uX2Zvb3RwcmludC90ZXN0X2NhcmJvbl9mb290cHJpbnQucHk=) | `99.66% <100.00%> (+0.01%)` | :arrow_up: |
| [...\_footprint/test\_product\_or\_sector\_specific\_rule.py](https://app.codecov.io/gh/ZeroTwentyFifty/pact_methodology/pull/51?src=pr&el=tree&filepath=tests%2Fcarbon_footprint%2Ftest_product_or_sector_specific_rule.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ZeroTwentyFifty#diff-dGVzdHMvY2FyYm9uX2Zvb3RwcmludC90ZXN0X3Byb2R1Y3Rfb3Jfc2VjdG9yX3NwZWNpZmljX3J1bGUucHk=) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/pact_methodology/assurance/assurance.py b/pact_methodology/assurance/assurance.py
index aecf469..8aa4921 100644
--- a/pact_methodology/assurance/assurance.py
+++ b/pact_methodology/assurance/assurance.py
@@ -9,23 +9,32 @@ class Coverage(str, Enum):
     PRODUCT_LINE = "product line"
     PRODUCT_LEVEL = "product level"
 
+    def __str__(self):
+        return self.value
+
 
 class Level(str, Enum):
     LIMITED = "limited"
     REASONABLE = "reasonable"
 
+    def __str__(self):
+        return self.value
+
 
 class Boundary(str, Enum):
     GATE_TO_GATE = "Gate-to-Gate"
     CRADLE_TO_GATE = "Cradle-to-Gate"
 
+    def __str__(self):
+        return self.value
+
 
 class Assurance:
     """
     Represents an assurance in conformance with Pathfinder Framework chapter 5 and appendix B.
 
     Args:
-        assurance (bool): A boolean flag indicating whether the CarbonFootprint has been assured in line with Pathfinder Framework requirements (section 5).
+        assurance (bool): A boolean flag indicating whether the CarbonFootprint has been assured in line with PACT Methodology requirements (section 5).
         provider_name (str): The non-empty name of the independent third party engaged to undertake the assurance.
         coverage (Coverage | None): Level of granularity of the emissions data assured.
         level (Level | None): Level of assurance applicable to the PCF.
@@ -35,7 +44,7 @@ class Assurance:
         comments (str | None): Any additional comments that will clarify the interpretation of the assurance.
 
     Attributes:
-        assurance (bool): A boolean flag indicating whether the CarbonFootprint has been assured in line with Pathfinder Framework requirements (section 5).
+        assurance (bool): A boolean flag indicating whether the CarbonFootprint has been assured in line with PACT Methodology requirements (section 5).
         provider_name (str): The non-empty name of the independent third party engaged to undertake the assurance.
         coverage (Coverage | None): Level of granularity of the emissions data assured.
         level (Level | None): Level of assurance applicable to the PCF.
@@ -110,3 +119,16 @@ class Assurance:
             assurance_dict["comments"] = self.comments
 
         return assurance_dict
+
+    def __str__(self):
+        return (
+            f"Assurance("
+            f"assurance={self.assurance}, "
+            f"provider_name='{self.provider_name}', "
+            f"coverage={self.coverage}, "
+            f"level={self.level}, "
+            f"boundary={self.boundary}, "
+            f"completed_at={self.completed_at}, "
+            f"standard_name='{self.standard_name}', "
+            f"comments='{self.comments}')"
+        )
\ No newline at end of file
diff --git a/pact_methodology/carbon_footprint/carbon_footprint.py b/pact_methodology/carbon_footprint/carbon_footprint.py
index e61648c..58fee31 100644
--- a/pact_methodology/carbon_footprint/carbon_footprint.py
+++ b/pact_methodology/carbon_footprint/carbon_footprint.py
@@ -448,3 +448,79 @@ class CarbonFootprint:
         if value is not None and not all(isinstance(rule, ProductOrSectorSpecificRule) for rule in value):
             raise ValueError("product_or_sector_specific_rules must be a list of ProductOrSectorSpecificRule")
         self._product_or_sector_specific_rules = value
+
+    def __str__(self):
+        return (
+            f"CarbonFootprint("
+            f"declared_unit={self.declared_unit}, "
+            f"unitary_product_amount={self.unitary_product_amount}, "
+            f"p_cf_excluding_biogenic={self.p_cf_excluding_biogenic}, "
+            f"p_cf_including_biogenic={self.p_cf_including_biogenic}, "
+            f"fossil_ghg_emissions={self.fossil_ghg_emissions}, "
+            f"fossil_carbon_content={self.fossil_carbon_content}, "
+            f"biogenic_carbon_content={self.biogenic_carbon_content}, "
+            f"characterization_factors={self.characterization_factors.value}, "
+            f"ipcc_characterization_factors_sources={self.ipcc_characterization_factors_sources}, "
+            f"cross_sectoral_standards_used={[str(standard) for standard in self.cross_sectoral_standards_used]}, "
+            f"boundary_processes_description='{self.boundary_processes_description}', "
+            f"exempted_emissions_percent={self.exempted_emissions_percent}, "
+            f"exempted_emissions_description='{self.exempted_emissions_description}', "
+            f"reference_period={self.reference_period}, "
+            f"packaging_emissions_included={self.packaging_emissions_included}, "
+            f"geographical_scope={self.geographical_scope}, "
+            f"primary_data_share={self.primary_data_share}, "
+            f"dqi={self.dqi}, "
+            f"d_luc_ghg_emissions={self.d_luc_ghg_emissions}, "
+            f"land_management_ghg_emissions={self.land_management_ghg_emissions}, "
+            f"other_biogenic_ghg_emissions={self.other_biogenic_ghg_emissions}, "
+            f"biogenic_carbon_withdrawal={self.biogenic_carbon_withdrawal}, "
+            f"iluc_ghg_emissions={self.iluc_ghg_emissions}, "
+            f"aircraft_ghg_emissions={self.aircraft_ghg_emissions}, "
+            f"packaging_ghg_emissions={self.packaging_ghg_emissions}, "
+            f"allocation_rules_description='{self.allocation_rules_description}', "
+            f"uncertainty_assessment_description='{self.uncertainty_assessment_description}', "
+            f"assurance={self.assurance}, "
+            f"biogenic_accounting_methodology={self.biogenic_accounting_methodology.value}, "
+            f"product_or_sector_specific_rules={[str(rule) for rule in self.product_or_sector_specific_rules]})"
+        )
+
+    def __repr__(self):
+        return (
+            f"CarbonFootprint("
+            f"declared_unit={self.declared_unit!r}, "
+            f"unitary_product_amount={self.unitary_product_amount!r}, "
+            f"p_cf_excluding_biogenic={self.p_cf_excluding_biogenic!r}, "
+            f"p_cf_including_biogenic={self.p_cf_including_biogenic!r}, "
+            f"fossil_ghg_emissions={self.fossil_ghg_emissions!r}, "
+            f"fossil_carbon_content={self.fossil_carbon_content!r}, "
+            f"biogenic_carbon_content={self.biogenic_carbon_content!r}, "
+            f"characterization_factors={self.characterization_factors!r}, "
+            f"ipcc_characterization_factors_sources={self.ipcc_characterization_factors_sources!r}, "
+            f"cross_sectoral_standards_used={self.cross_sectoral_standards_used!r}, "
+            f"boundary_processes_description={self.boundary_processes_description!r}, "
+            f"exempted_emissions_percent={self.exempted_emissions_percent!r}, "
+            f"exempted_emissions_description={self.exempted_emissions_description!r}, "
+            f"reference_period={self.reference_period!r}, "
+            f"packaging_emissions_included={self.packaging_emissions_included!r}, "
+            f"geographical_scope={self.geographical_scope!r}, "
+            f"primary_data_share={self.primary_data_share!r}, "
+            f"dqi={self.dqi!r}, "
+            f"d_luc_ghg_emissions={self.d_luc_ghg_emissions!r}, "
+            f"land_management_ghg_emissions={self.land_management_ghg_emissions!r}, "
+            f"other_biogenic_ghg_emissions={self.other_biogenic_ghg_emissions!r}, "
+            f"biogenic_carbon_withdrawal={self.biogenic_carbon_withdrawal!r}, "
+            f"iluc_ghg_emissions={self.iluc_ghg_emissions!r}, "
+            f"aircraft_ghg_emissions={self.aircraft_ghg_emissions!r}, "
+            f"packaging_ghg_emissions={self.packaging_ghg_emissions!r}, "
+            f"allocation_rules_description={self.allocation_rules_description!r}, "
+            f"uncertainty_assessment_description={self.uncertainty_assessment_description!r}, "
+            f"assurance={self.assurance!r}, "
+            f"biogenic_accounting_methodology={self.biogenic_accounting_methodology!r}, "
+            f"product_or_sector_specific_rules={self.product_or_sector_specific_rules!r})"
+        )
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
diff --git a/pact_methodology/carbon_footprint/product_or_sector_specific_rule.py b/pact_methodology/carbon_footprint/product_or_sector_specific_rule.py
index 0e49b5b..9724d16 100644
--- a/pact_methodology/carbon_footprint/product_or_sector_specific_rule.py
+++ b/pact_methodology/carbon_footprint/product_or_sector_specific_rule.py
@@ -170,7 +170,7 @@ class ProductOrSectorSpecificRule:
             A human-readable string showing the rule's attributes
         """
         return (
-            f"[operator={self.operator}, rule_names={self.rule_names}, other_operator_name={self.other_operator_name}]"
+            f"operator={self.operator}, rule_names={self.rule_names}, other_operator_name={self.other_operator_name}"
         )
     
     def __repr__(self) -> str:


Test patch:
diff --git a/tests/assurance/test_assurance.py b/tests/assurance/test_assurance.py
index ec1dc0e..6ac9159 100644
--- a/tests/assurance/test_assurance.py
+++ b/tests/assurance/test_assurance.py
@@ -146,3 +146,26 @@ def test_assurance_init_with_invalid_comments(comments):
     with pytest.raises(ValueError) as excinfo:
         Assurance(True, "My Auditor", comments=comments)
     assert str(excinfo.value) == "comments must be a string"
+
+def test_assurance_str():
+    assurance = Assurance(
+        assurance=True,
+        provider_name="Example Provider",
+        coverage=Coverage.PCF_SYSTEM,
+        level=Level.REASONABLE,
+        boundary=Boundary.GATE_TO_GATE,
+        completed_at=DateTime("2023-01-01T00:00:00Z"),
+        standard_name="Example Standard",
+        comments="Example comments",
+    )
+    assert str(assurance) == (
+        f"Assurance("
+        f"assurance=True, "
+        f"provider_name='Example Provider', "
+        f"coverage=PCF system, "
+        f"level=reasonable, "
+        f"boundary=Gate-to-Gate, "
+        f"completed_at=2023-01-01T00:00:00Z, "
+        f"standard_name='Example Standard', "
+        f"comments='Example comments')"
+    )
\ No newline at end of file
diff --git a/tests/carbon_footprint/test_carbon_footprint.py b/tests/carbon_footprint/test_carbon_footprint.py
index 5a67ae2..441fde1 100644
--- a/tests/carbon_footprint/test_carbon_footprint.py
+++ b/tests/carbon_footprint/test_carbon_footprint.py
@@ -21,7 +21,7 @@ from pact_methodology.carbon_footprint.geographical_scope import (
     GeographicalGranularity,
 )
 from pact_methodology.data_quality_indicators.data_quality_indicators import (
-    DataQualityIndicators,
+    DataQualityIndicators, DataQualityRating
 )
 from pact_methodology.carbon_footprint.biogenic_accounting_methodology import BiogenicAccountingMethodology
 from pact_methodology.carbon_footprint.product_or_sector_specific_rule import ProductOrSectorSpecificRule
@@ -44,7 +44,7 @@ def valid_carbon_footprint_data():
         "boundary_processes_description": "boundary processes description",
         "exempted_emissions_percent": 1.0,
         "exempted_emissions_description": "Rationale for exclusion",
-        "reference_period": ReferencePeriod(start=DateTime.now(), end=DateTime.now()),
+        "reference_period": ReferencePeriod(start=DateTime("2022-01-01T00:00:00Z"), end=DateTime("2022-12-31T23:59:59Z")),
         "packaging_emissions_included": True,
         "geographical_scope": CarbonFootprintGeographicalScope(
             global_scope=True,
@@ -54,7 +54,13 @@ def valid_carbon_footprint_data():
         ),
         "primary_data_share": 50.0,
         "dqi": DataQualityIndicators(
-            reference_period=ReferencePeriod(start=DateTime.now(), end=DateTime.now())
+            reference_period=ReferencePeriod(start=DateTime("2022-01-01T00:00:00Z"), end=DateTime("2022-12-31T23:59:59Z")),
+            coverage_percent=80.0,
+            technological_dqr=DataQualityRating(2),
+            temporal_dqr=DataQualityRating(2),
+            geographical_dqr=DataQualityRating(1),
+            completeness_dqr=DataQualityRating(2),
+            reliability_dqr=DataQualityRating(3),
         ),
         "d_luc_ghg_emissions": 2,
         "land_management_ghg_emissions": 1.0,
@@ -798,3 +804,93 @@ def test_carbon_footprint_invalid_product_or_sector_specific_rules(
     with pytest.raises(ValueError) as excinfo:
         CarbonFootprint(**invalid_data)
     assert str(excinfo.value) == expected_error
+
+def test_carbon_footprint_str(valid_carbon_footprint_data):
+    carbon_footprint = CarbonFootprint(**valid_carbon_footprint_data)
+    assert str(carbon_footprint) == (
+        f"CarbonFootprint("
+        f"declared_unit=kilogram, "
+        f"unitary_product_amount=1.0, "
+        f"p_cf_excluding_biogenic=0.5, "
+        f"p_cf_including_biogenic=2, "
+        f"fossil_ghg_emissions=0.3, "
+        f"fossil_carbon_content=0.2, "
+        f"biogenic_carbon_content=0.1, "
+        f"characterization_factors=AR6, "
+        f"ipcc_characterization_factors_sources=['AR6'], "
+        f"cross_sectoral_standards_used=['GHG Protocol Product standard'], "
+        f"boundary_processes_description='boundary processes description', "
+        f"exempted_emissions_percent=1.0, "
+        f"exempted_emissions_description='Rationale for exclusion', "
+        f"reference_period=ReferencePeriod(start=2022-01-01T00:00:00Z, end=2022-12-31T23:59:59Z), "
+        f"packaging_emissions_included=True, "
+        f"geographical_scope=Geographical scope: Global (at Global level), "
+        f"primary_data_share=50.0, "
+        f"dqi=DataQualityIndicators("
+        f"reference_period=ReferencePeriod(start=2022-01-01T00:00:00Z, end=2022-12-31T23:59:59Z), "
+        f"coverage_percent=80.0, "
+        f"technological_dqr=2, "
+        f"temporal_dqr=2, "
+        f"geographical_dqr=1, "
+        f"completeness_dqr=2, "
+        f"reliability_dqr=3), "
+        f"d_luc_ghg_emissions=2, "
+        f"land_management_ghg_emissions=1.0, "
+        f"other_biogenic_ghg_emissions=1.5, "
+        f"biogenic_carbon_withdrawal=-1.0, "
+        f"iluc_ghg_emissions=1.0, "
+        f"aircraft_ghg_emissions=1.0, "
+        f"packaging_ghg_emissions=1.0, "
+        f"allocation_rules_description='Example allocation rules description', "
+        f"uncertainty_assessment_description='Example uncertainty assessment description', "
+        f"assurance=Assurance(assurance=True, provider_name='Example provider name', coverage=PCF system, "
+        f"level=reasonable, boundary=Gate-to-Gate, completed_at={carbon_footprint.assurance.completed_at}, "
+        f"standard_name='Example standard name', comments='Example comments'), "
+        f"biogenic_accounting_methodology=GHGP, "
+        f"product_or_sector_specific_rules=[\"operator=Other, rule_names=['Rule1'], other_operator_name=Custom Operator\"])"
+    )
+
+def test_carbon_footprint_repr(valid_carbon_footprint_data):
+    carbon_footprint = CarbonFootprint(**valid_carbon_footprint_data)
+    assert repr(carbon_footprint) == (
+        f"CarbonFootprint(declared_unit={carbon_footprint.declared_unit!r}, "
+        f"unitary_product_amount={carbon_footprint.unitary_product_amount!r}, "
+        f"p_cf_excluding_biogenic={carbon_footprint.p_cf_excluding_biogenic!r}, "
+        f"p_cf_including_biogenic={carbon_footprint.p_cf_including_biogenic!r}, "
+        f"fossil_ghg_emissions={carbon_footprint.fossil_ghg_emissions!r}, "
+        f"fossil_carbon_content={carbon_footprint.fossil_carbon_content!r}, "
+        f"biogenic_carbon_content={carbon_footprint.biogenic_carbon_content!r}, "
+        f"characterization_factors={carbon_footprint.characterization_factors!r}, "
+        f"ipcc_characterization_factors_sources={carbon_footprint.ipcc_characterization_factors_sources!r}, "
+        f"cross_sectoral_standards_used={carbon_footprint.cross_sectoral_standards_used!r}, "
+        f"boundary_processes_description={carbon_footprint.boundary_processes_description!r}, "
+        f"exempted_emissions_percent={carbon_footprint.exempted_emissions_percent!r}, "
+        f"exempted_emissions_description={carbon_footprint.exempted_emissions_description!r}, "
+        f"reference_period={carbon_footprint.reference_period!r}, "
+        f"packaging_emissions_included={carbon_footprint.packaging_emissions_included!r}, "
+        f"geographical_scope={carbon_footprint.geographical_scope!r}, "
+        f"primary_data_share={carbon_footprint.primary_data_share!r}, "
+        f"dqi={carbon_footprint.dqi!r}, "
+        f"d_luc_ghg_emissions={carbon_footprint.d_luc_ghg_emissions!r}, "
+        f"land_management_ghg_emissions={carbon_footprint.land_management_ghg_emissions!r}, "
+        f"other_biogenic_ghg_emissions={carbon_footprint.other_biogenic_ghg_emissions!r}, "
+        f"biogenic_carbon_withdrawal={carbon_footprint.biogenic_carbon_withdrawal!r}, "
+        f"iluc_ghg_emissions={carbon_footprint.iluc_ghg_emissions!r}, "
+        f"aircraft_ghg_emissions={carbon_footprint.aircraft_ghg_emissions!r}, "
+        f"packaging_ghg_emissions={carbon_footprint.packaging_ghg_emissions!r}, "
+        f"allocation_rules_description={carbon_footprint.allocation_rules_description!r}, "
+        f"uncertainty_assessment_description={carbon_footprint.uncertainty_assessment_description!r}, "
+        f"assurance={carbon_footprint.assurance!r}, "
+        f"biogenic_accounting_methodology={carbon_footprint.biogenic_accounting_methodology!r}, "
+        f"product_or_sector_specific_rules={carbon_footprint.product_or_sector_specific_rules!r})"
+    )
+
+def test_carbon_footprint_eq(valid_carbon_footprint_data):
+    carbon_footprint1 = CarbonFootprint(**valid_carbon_footprint_data)
+    carbon_footprint2 = CarbonFootprint(**valid_carbon_footprint_data)
+    assert carbon_footprint1 == carbon_footprint2
+
+def test_carbon_footprint_not_eq(valid_carbon_footprint_data):
+    carbon_footprint1 = CarbonFootprint(**valid_carbon_footprint_data)
+    carbon_footprint2 = CarbonFootprint(**{**valid_carbon_footprint_data, "unitary_product_amount": 2.0})
+    assert carbon_footprint1 != carbon_footprint2
diff --git a/tests/carbon_footprint/test_product_or_sector_specific_rule.py b/tests/carbon_footprint/test_product_or_sector_specific_rule.py
index 40d7b3f..b62e98c 100644
--- a/tests/carbon_footprint/test_product_or_sector_specific_rule.py
+++ b/tests/carbon_footprint/test_product_or_sector_specific_rule.py
@@ -84,7 +84,7 @@ def test_product_or_sector_specific_rule_to_dict(valid_product_or_sector_specifi
 
 def test_product_or_sector_specific_rule_str(valid_product_or_sector_specific_rule):
     rule = ProductOrSectorSpecificRule(**valid_product_or_sector_specific_rule)
-    expected_str = "[operator=Other, rule_names=['Rule1'], other_operator_name=Custom Operator]"
+    expected_str = "operator=Other, rule_names=['Rule1'], other_operator_name=Custom Operator"
     assert str(rule) == expected_str