c2pa-python 0.25.0__tar.gz → 0.27.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {c2pa_python-0.25.0/src/c2pa_python.egg-info → c2pa_python-0.27.0}/PKG-INFO +1 -1
  2. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/pyproject.toml +1 -1
  3. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/c2pa.py +243 -11
  4. {c2pa_python-0.25.0 → c2pa_python-0.27.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  5. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/tests/test_unit_tests.py +865 -1
  6. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/tests/test_unit_tests_threaded.py +150 -0
  7. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/LICENSE-APACHE +0 -0
  8. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/LICENSE-MIT +0 -0
  9. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/MANIFEST.in +0 -0
  10. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/README.md +0 -0
  11. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/requirements.txt +0 -0
  12. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/scripts/download_artifacts.py +0 -0
  13. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/setup.cfg +0 -0
  14. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/setup.py +0 -0
  15. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/__init__.py +0 -0
  16. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/build.py +0 -0
  17. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/lib.py +0 -0
  18. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  19. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  20. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  21. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/requires.txt +0 -0
  22. {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.25.0
3
+ Version: 0.27.0
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
6
  Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "c2pa-python"
7
- version = "0.25.0"
7
+ version = "0.27.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -49,6 +49,7 @@ _REQUIRED_FUNCTIONS = [
49
49
  'c2pa_builder_set_remote_url',
50
50
  'c2pa_builder_add_resource',
51
51
  'c2pa_builder_add_ingredient_from_stream',
52
+ 'c2pa_builder_add_action',
52
53
  'c2pa_builder_to_archive',
53
54
  'c2pa_builder_sign',
54
55
  'c2pa_manifest_bytes_free',
@@ -403,6 +404,9 @@ _setup_function(_lib.c2pa_builder_add_ingredient_from_stream,
403
404
  ctypes.c_char_p,
404
405
  ctypes.POINTER(C2paStream)],
405
406
  ctypes.c_int)
407
+ _setup_function(_lib.c2pa_builder_add_action,
408
+ [ctypes.POINTER(C2paBuilder), ctypes.c_char_p],
409
+ ctypes.c_int)
406
410
  _setup_function(_lib.c2pa_builder_to_archive,
407
411
  [ctypes.POINTER(C2paBuilder), ctypes.POINTER(C2paStream)],
408
412
  ctypes.c_int)
@@ -667,20 +671,44 @@ def version() -> str:
667
671
  return _convert_to_py_string(result)
668
672
 
669
673
 
674
+ @overload
670
675
  def load_settings(settings: str, format: str = "json") -> None:
671
- """Load C2PA settings from a string.
676
+ ...
677
+
678
+
679
+ @overload
680
+ def load_settings(settings: dict) -> None:
681
+ ...
682
+
683
+
684
+ def load_settings(settings: Union[str, dict], format: str = "json") -> None:
685
+ """Load C2PA settings from a string or dict.
672
686
 
673
687
  Args:
674
- settings: The settings string to load
675
- format: The format of the settings string (default: "json")
688
+ settings: The settings string or dict to load
689
+ format: The format of the settings string (default: "json").
690
+ Ignored when settings is a dict.
676
691
 
677
692
  Raises:
678
693
  C2paError: If there was an error loading the settings
679
694
  """
680
- result = _lib.c2pa_load_settings(
681
- settings.encode('utf-8'),
682
- format.encode('utf-8')
683
- )
695
+ # Convert to JSON string as necessary
696
+ try:
697
+ if isinstance(settings, dict):
698
+ settings_str = json.dumps(settings)
699
+ format = "json"
700
+ else:
701
+ settings_str = settings
702
+ except (TypeError, ValueError) as e:
703
+ raise C2paError(f"Failed to serialize settings to JSON: {e}")
704
+
705
+ try:
706
+ settings_bytes = settings_str.encode('utf-8')
707
+ format_bytes = format.encode('utf-8')
708
+ except (AttributeError, UnicodeEncodeError) as e:
709
+ raise C2paError(f"Failed to encode settings to UTF-8: {e}")
710
+
711
+ result = _lib.c2pa_load_settings(settings_bytes, format_bytes)
684
712
  if result != 0:
685
713
  error = _parse_operation_result_for_error(_lib.c2pa_error())
686
714
  if error:
@@ -1358,6 +1386,10 @@ class Reader:
1358
1386
  # we may have opened ourselves, and that we need to close later
1359
1387
  self._backing_file = None
1360
1388
 
1389
+ # Caches for manifest JSON string and parsed data
1390
+ self._manifest_json_str_cache = None
1391
+ self._manifest_data_cache = None
1392
+
1361
1393
  if stream is None:
1362
1394
  # If we don't get a stream as param:
1363
1395
  # Create a stream from the file path in format_or_path
@@ -1600,6 +1632,33 @@ class Reader:
1600
1632
  # Ensure we don't raise exceptions during cleanup
1601
1633
  pass
1602
1634
 
1635
+ def _get_cached_manifest_data(self) -> Optional[dict]:
1636
+ """Get the cached manifest data, fetching and parsing if not cached.
1637
+
1638
+ Returns:
1639
+ A dictionary containing the parsed manifest data, or None if
1640
+ JSON parsing fails
1641
+
1642
+ Raises:
1643
+ C2paError: If there was an error getting the JSON
1644
+ """
1645
+ if self._manifest_data_cache is None:
1646
+ if self._manifest_json_str_cache is None:
1647
+ self._manifest_json_str_cache = self.json()
1648
+
1649
+ try:
1650
+ self._manifest_data_cache = json.loads(
1651
+ self._manifest_json_str_cache
1652
+ )
1653
+ except json.JSONDecodeError:
1654
+ # Reset cache to reattempt read, possibly
1655
+ self._manifest_data_cache = None
1656
+ self._manifest_json_str_cache = None
1657
+ # Failed to parse manifest JSON
1658
+ return None
1659
+
1660
+ return self._manifest_data_cache
1661
+
1603
1662
  def close(self):
1604
1663
  """Release the reader resources.
1605
1664
 
@@ -1620,6 +1679,9 @@ class Reader:
1620
1679
  Reader._ERROR_MESSAGES['cleanup_error'].format(
1621
1680
  str(e)))
1622
1681
  finally:
1682
+ # Clear the cache when closing
1683
+ self._manifest_json_str_cache = None
1684
+ self._manifest_data_cache = None
1623
1685
  self._closed = True
1624
1686
 
1625
1687
  def json(self) -> str:
@@ -1634,6 +1696,10 @@ class Reader:
1634
1696
 
1635
1697
  self._ensure_valid_state()
1636
1698
 
1699
+ # Return cached result if available
1700
+ if self._manifest_json_str_cache is not None:
1701
+ return self._manifest_json_str_cache
1702
+
1637
1703
  result = _lib.c2pa_reader_json(self._reader)
1638
1704
 
1639
1705
  if result is None:
@@ -1642,7 +1708,128 @@ class Reader:
1642
1708
  raise C2paError(error)
1643
1709
  raise C2paError("Error during manifest parsing in Reader")
1644
1710
 
1645
- return _convert_to_py_string(result)
1711
+ # Cache the result and return it
1712
+ self._manifest_json_str_cache = _convert_to_py_string(result)
1713
+ return self._manifest_json_str_cache
1714
+
1715
+ def get_active_manifest(self) -> Optional[dict]:
1716
+ """Get the active manifest from the manifest store.
1717
+
1718
+ This method retrieves the full manifest JSON and extracts the active
1719
+ manifest based on the active_manifest key.
1720
+
1721
+ Returns:
1722
+ A dictionary containing the active manifest data, including claims,
1723
+ assertions, ingredients, and signature information, or None if no
1724
+ manifest is found or if there was an error parsing the JSON.
1725
+
1726
+ Raises:
1727
+ KeyError: If the active_manifest key is missing from the JSON
1728
+ """
1729
+ try:
1730
+ # Get cached manifest data
1731
+ manifest_data = self._get_cached_manifest_data()
1732
+ if manifest_data is None:
1733
+ # raise C2paError("Failed to parse manifest JSON")
1734
+ return None
1735
+
1736
+ # Get the active manfiest id/label
1737
+ if "active_manifest" not in manifest_data:
1738
+ raise KeyError("No 'active_manifest' key found")
1739
+
1740
+ active_manifest_id = manifest_data["active_manifest"]
1741
+
1742
+ # Retrieve the active manifest data using manifest id/label
1743
+ if "manifests" not in manifest_data:
1744
+ raise KeyError("No 'manifests' key found in manifest data")
1745
+
1746
+ manifests = manifest_data["manifests"]
1747
+ if active_manifest_id not in manifests:
1748
+ raise KeyError("Active manifest not found in manifest store")
1749
+
1750
+ return manifests[active_manifest_id]
1751
+ except C2paError.ManifestNotFound:
1752
+ return None
1753
+
1754
+ def get_manifest(self, label: str) -> Optional[dict]:
1755
+ """Get a specific manifest from the manifest store by its label.
1756
+
1757
+ This method retrieves the manifest JSON and extracts the manifest
1758
+ that corresponds to the provided manifest label/ID.
1759
+
1760
+ Args:
1761
+ label: The manifest label/ID to look up in the manifest store
1762
+
1763
+ Returns:
1764
+ A dictionary containing the manifest data for the specified label,
1765
+ or None if no manifest is found or if there was an error parsing
1766
+ the JSON.
1767
+
1768
+ Raises:
1769
+ KeyError: If the manifests key is missing from the JSON
1770
+ """
1771
+ try:
1772
+ # Get cached manifest data
1773
+ manifest_data = self._get_cached_manifest_data()
1774
+ if manifest_data is None:
1775
+ # raise C2paError("Failed to parse manifest JSON")
1776
+ return None
1777
+
1778
+ if "manifests" not in manifest_data:
1779
+ raise KeyError("No 'manifests' key found in manifest data")
1780
+
1781
+ manifests = manifest_data["manifests"]
1782
+ if label not in manifests:
1783
+ raise KeyError(f"Manifest {label} not found in manifest store")
1784
+
1785
+ return manifests[label]
1786
+ except C2paError.ManifestNotFound:
1787
+ return None
1788
+
1789
+ def get_validation_state(self) -> Optional[str]:
1790
+ """Get the validation state of the manifest store.
1791
+
1792
+ This method retrieves the full manifest JSON and extracts the
1793
+ validation_state field, which indicates the overall validation
1794
+ status of the C2PA manifest.
1795
+
1796
+ Returns:
1797
+ The validation state as a string,
1798
+ or None if the validation_state field is not present or if no
1799
+ manifest is found or if there was an error parsing the JSON.
1800
+ """
1801
+ try:
1802
+ # Get cached manifest data
1803
+ manifest_data = self._get_cached_manifest_data()
1804
+ if manifest_data is None:
1805
+ return None
1806
+
1807
+ return manifest_data.get("validation_state")
1808
+ except C2paError.ManifestNotFound:
1809
+ return None
1810
+
1811
+ def get_validation_results(self) -> Optional[dict]:
1812
+ """Get the validation results of the manifest store.
1813
+
1814
+ This method retrieves the full manifest JSON and extracts
1815
+ the validation_results object, which contains detailed
1816
+ validation information.
1817
+
1818
+ Returns:
1819
+ The validation results as a dictionary containing
1820
+ validation details, or None if the validation_results
1821
+ field is not present or if no manifest is found or if
1822
+ there was an error parsing the JSON.
1823
+ """
1824
+ try:
1825
+ # Get cached manifest data
1826
+ manifest_data = self._get_cached_manifest_data()
1827
+ if manifest_data is None:
1828
+ return None
1829
+
1830
+ return manifest_data.get("validation_results")
1831
+ except C2paError.ManifestNotFound:
1832
+ return None
1646
1833
 
1647
1834
  def resource_to_stream(self, uri: str, stream: Any) -> int:
1648
1835
  """Write a resource to a stream.
@@ -2039,6 +2226,7 @@ class Builder:
2039
2226
  'url_error': "Error setting remote URL: {}",
2040
2227
  'resource_error': "Error adding resource: {}",
2041
2228
  'ingredient_error': "Error adding ingredient: {}",
2229
+ 'action_error': "Error adding action: {}",
2042
2230
  'archive_error': "Error writing archive: {}",
2043
2231
  'sign_error': "Error during signing: {}",
2044
2232
  'encoding_error': "Invalid UTF-8 characters in manifest: {}",
@@ -2336,13 +2524,16 @@ class Builder:
2336
2524
  )
2337
2525
  )
2338
2526
 
2339
- def add_ingredient(self, ingredient_json: str, format: str, source: Any):
2527
+ def add_ingredient(
2528
+ self, ingredient_json: Union[str, dict], format: str, source: Any
2529
+ ):
2340
2530
  """Add an ingredient to the builder (facade method).
2341
2531
  The added ingredient's source should be a stream-like object
2342
2532
  (for instance, a file opened as stream).
2343
2533
 
2344
2534
  Args:
2345
2535
  ingredient_json: The JSON ingredient definition
2536
+ (either a JSON string or a dictionary)
2346
2537
  format: The MIME type or extension of the ingredient
2347
2538
  source: The stream containing the ingredient data
2348
2539
  (any Python stream-like object)
@@ -2362,7 +2553,7 @@ class Builder:
2362
2553
 
2363
2554
  def add_ingredient_from_stream(
2364
2555
  self,
2365
- ingredient_json: str,
2556
+ ingredient_json: Union[str, dict],
2366
2557
  format: str,
2367
2558
  source: Any):
2368
2559
  """Add an ingredient from a stream to the builder.
@@ -2370,6 +2561,7 @@ class Builder:
2370
2561
 
2371
2562
  Args:
2372
2563
  ingredient_json: The JSON ingredient definition
2564
+ (either a JSON string or a dictionary)
2373
2565
  format: The MIME type or extension of the ingredient
2374
2566
  source: The stream containing the ingredient data
2375
2567
  (any Python stream-like object)
@@ -2381,6 +2573,9 @@ class Builder:
2381
2573
  """
2382
2574
  self._ensure_valid_state()
2383
2575
 
2576
+ if isinstance(ingredient_json, dict):
2577
+ ingredient_json = json.dumps(ingredient_json)
2578
+
2384
2579
  try:
2385
2580
  ingredient_str = ingredient_json.encode('utf-8')
2386
2581
  format_str = format.encode('utf-8')
@@ -2411,7 +2606,7 @@ class Builder:
2411
2606
 
2412
2607
  def add_ingredient_from_file_path(
2413
2608
  self,
2414
- ingredient_json: str,
2609
+ ingredient_json: Union[str, dict],
2415
2610
  format: str,
2416
2611
  filepath: Union[str, Path]):
2417
2612
  """Add an ingredient from a file path to the builder (deprecated).
@@ -2423,6 +2618,7 @@ class Builder:
2423
2618
 
2424
2619
  Args:
2425
2620
  ingredient_json: The JSON ingredient definition
2621
+ (either a JSON string or a dictionary)
2426
2622
  format: The MIME type or extension of the ingredient
2427
2623
  filepath: The path to the file containing the ingredient data
2428
2624
  (can be a string or Path object)
@@ -2454,6 +2650,42 @@ class Builder:
2454
2650
  except Exception as e:
2455
2651
  raise C2paError.Other(f"Could not add ingredient: {e}") from e
2456
2652
 
2653
+ def add_action(self, action_json: Union[str, dict]) -> None:
2654
+ """Add an action to the builder, that will be placed
2655
+ in the actions assertion array in the generated manifest.
2656
+
2657
+ Args:
2658
+ action_json: The JSON action definition
2659
+ (either a JSON string or a dictionary)
2660
+
2661
+ Raises:
2662
+ C2paError: If there was an error adding the action
2663
+ C2paError.Encoding: If the action JSON contains invalid UTF-8 chars
2664
+ """
2665
+ self._ensure_valid_state()
2666
+
2667
+ if isinstance(action_json, dict):
2668
+ action_json = json.dumps(action_json)
2669
+
2670
+ try:
2671
+ action_str = action_json.encode('utf-8')
2672
+ except UnicodeError as e:
2673
+ raise C2paError.Encoding(
2674
+ Builder._ERROR_MESSAGES['encoding_error'].format(str(e))
2675
+ )
2676
+
2677
+ result = _lib.c2pa_builder_add_action(self._builder, action_str)
2678
+
2679
+ if result != 0:
2680
+ error = _parse_operation_result_for_error(_lib.c2pa_error())
2681
+ if error:
2682
+ raise C2paError(error)
2683
+ raise C2paError(
2684
+ Builder._ERROR_MESSAGES['action_error'].format(
2685
+ "Unknown error"
2686
+ )
2687
+ )
2688
+
2457
2689
  def to_archive(self, stream: Any) -> None:
2458
2690
  """Write an archive of the builder to a stream.
2459
2691
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.25.0
3
+ Version: 0.27.0
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
6
  Maintainer-email: Gavin Peacock <gpeacock@adobe.com>