Skip to content

Commit 73697b7

Browse files
authored
feat: Add convenience APIs (#179)
* fix: Add some more APIs * fix: Add some more APIs 2 * fix: Active manifest API * fix: Convenience APIs * fix: Clean up * fix: Clean up 2 * fix: Some more convenience methods * fix: Test clean up * fix: Test clean up 2 * fix: Clean up * fix: Reader caching * fix: Update formats * fix: Workflow, add a check-format step * fix: Rename the flow * fix: Do not cache error states * fix: Rename * fix: Prepare version bump
1 parent 4593aac commit 73697b7

File tree

5 files changed

+570
-2
lines changed

5 files changed

+570
-2
lines changed

.github/workflows/build.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ jobs:
3636
id: read-version
3737
run: echo "version=$(cat c2pa-native-version.txt | tr -d '\r\n')" >> $GITHUB_OUTPUT
3838

39+
check-format:
40+
name: Check code format
41+
runs-on: ubuntu-latest
42+
steps:
43+
- name: Checkout repository
44+
uses: actions/checkout@v4
45+
46+
- name: Set up Python
47+
uses: actions/setup-python@v5
48+
with:
49+
python-version: "3.10"
50+
51+
- name: Install development dependencies
52+
run: python -m pip install -r requirements-dev.txt
53+
54+
- name: Check Python syntax
55+
run: python3 -m py_compile src/c2pa/c2pa.py
56+
continue-on-error: true
57+
58+
- name: Check code style with flake8
59+
run: flake8 src/c2pa/c2pa.py
60+
continue-on-error: true
61+
3962
tests-unix:
4063
name: Unit tests for developer setup (Unix)
4164
needs: read-version

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "c2pa-python"
7-
version = "0.25.0"
7+
version = "0.26.0"
88
requires-python = ">=3.10"
99
description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
1010
readme = { file = "README.md", content-type = "text/markdown" }

src/c2pa/c2pa.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,10 @@ def __init__(self,
13581358
# we may have opened ourselves, and that we need to close later
13591359
self._backing_file = None
13601360

1361+
# Caches for manifest JSON string and parsed data
1362+
self._manifest_json_str_cache = None
1363+
self._manifest_data_cache = None
1364+
13611365
if stream is None:
13621366
# If we don't get a stream as param:
13631367
# Create a stream from the file path in format_or_path
@@ -1600,6 +1604,33 @@ def _cleanup_resources(self):
16001604
# Ensure we don't raise exceptions during cleanup
16011605
pass
16021606

1607+
def _get_cached_manifest_data(self) -> Optional[dict]:
1608+
"""Get the cached manifest data, fetching and parsing if not cached.
1609+
1610+
Returns:
1611+
A dictionary containing the parsed manifest data, or None if
1612+
JSON parsing fails
1613+
1614+
Raises:
1615+
C2paError: If there was an error getting the JSON
1616+
"""
1617+
if self._manifest_data_cache is None:
1618+
if self._manifest_json_str_cache is None:
1619+
self._manifest_json_str_cache = self.json()
1620+
1621+
try:
1622+
self._manifest_data_cache = json.loads(
1623+
self._manifest_json_str_cache
1624+
)
1625+
except json.JSONDecodeError:
1626+
# Reset cache to reattempt read, possibly
1627+
self._manifest_data_cache = None
1628+
self._manifest_json_str_cache = None
1629+
# Failed to parse manifest JSON
1630+
return None
1631+
1632+
return self._manifest_data_cache
1633+
16031634
def close(self):
16041635
"""Release the reader resources.
16051636
@@ -1620,6 +1651,9 @@ def close(self):
16201651
Reader._ERROR_MESSAGES['cleanup_error'].format(
16211652
str(e)))
16221653
finally:
1654+
# Clear the cache when closing
1655+
self._manifest_json_str_cache = None
1656+
self._manifest_data_cache = None
16231657
self._closed = True
16241658

16251659
def json(self) -> str:
@@ -1634,6 +1668,10 @@ def json(self) -> str:
16341668

16351669
self._ensure_valid_state()
16361670

1671+
# Return cached result if available
1672+
if self._manifest_json_str_cache is not None:
1673+
return self._manifest_json_str_cache
1674+
16371675
result = _lib.c2pa_reader_json(self._reader)
16381676

16391677
if result is None:
@@ -1642,7 +1680,128 @@ def json(self) -> str:
16421680
raise C2paError(error)
16431681
raise C2paError("Error during manifest parsing in Reader")
16441682

1645-
return _convert_to_py_string(result)
1683+
# Cache the result and return it
1684+
self._manifest_json_str_cache = _convert_to_py_string(result)
1685+
return self._manifest_json_str_cache
1686+
1687+
def get_active_manifest(self) -> Optional[dict]:
1688+
"""Get the active manifest from the manifest store.
1689+
1690+
This method retrieves the full manifest JSON and extracts the active
1691+
manifest based on the active_manifest key.
1692+
1693+
Returns:
1694+
A dictionary containing the active manifest data, including claims,
1695+
assertions, ingredients, and signature information, or None if no
1696+
manifest is found or if there was an error parsing the JSON.
1697+
1698+
Raises:
1699+
KeyError: If the active_manifest key is missing from the JSON
1700+
"""
1701+
try:
1702+
# Get cached manifest data
1703+
manifest_data = self._get_cached_manifest_data()
1704+
if manifest_data is None:
1705+
# raise C2paError("Failed to parse manifest JSON")
1706+
return None
1707+
1708+
# Get the active manfiest id/label
1709+
if "active_manifest" not in manifest_data:
1710+
raise KeyError("No 'active_manifest' key found")
1711+
1712+
active_manifest_id = manifest_data["active_manifest"]
1713+
1714+
# Retrieve the active manifest data using manifest id/label
1715+
if "manifests" not in manifest_data:
1716+
raise KeyError("No 'manifests' key found in manifest data")
1717+
1718+
manifests = manifest_data["manifests"]
1719+
if active_manifest_id not in manifests:
1720+
raise KeyError("Active manifest not found in manifest store")
1721+
1722+
return manifests[active_manifest_id]
1723+
except C2paError.ManifestNotFound:
1724+
return None
1725+
1726+
def get_manifest(self, label: str) -> Optional[dict]:
1727+
"""Get a specific manifest from the manifest store by its label.
1728+
1729+
This method retrieves the manifest JSON and extracts the manifest
1730+
that corresponds to the provided manifest label/ID.
1731+
1732+
Args:
1733+
label: The manifest label/ID to look up in the manifest store
1734+
1735+
Returns:
1736+
A dictionary containing the manifest data for the specified label,
1737+
or None if no manifest is found or if there was an error parsing
1738+
the JSON.
1739+
1740+
Raises:
1741+
KeyError: If the manifests key is missing from the JSON
1742+
"""
1743+
try:
1744+
# Get cached manifest data
1745+
manifest_data = self._get_cached_manifest_data()
1746+
if manifest_data is None:
1747+
# raise C2paError("Failed to parse manifest JSON")
1748+
return None
1749+
1750+
if "manifests" not in manifest_data:
1751+
raise KeyError("No 'manifests' key found in manifest data")
1752+
1753+
manifests = manifest_data["manifests"]
1754+
if label not in manifests:
1755+
raise KeyError(f"Manifest {label} not found in manifest store")
1756+
1757+
return manifests[label]
1758+
except C2paError.ManifestNotFound:
1759+
return None
1760+
1761+
def get_validation_state(self) -> Optional[str]:
1762+
"""Get the validation state of the manifest store.
1763+
1764+
This method retrieves the full manifest JSON and extracts the
1765+
validation_state field, which indicates the overall validation
1766+
status of the C2PA manifest.
1767+
1768+
Returns:
1769+
The validation state as a string,
1770+
or None if the validation_state field is not present or if no
1771+
manifest is found or if there was an error parsing the JSON.
1772+
"""
1773+
try:
1774+
# Get cached manifest data
1775+
manifest_data = self._get_cached_manifest_data()
1776+
if manifest_data is None:
1777+
return None
1778+
1779+
return manifest_data.get("validation_state")
1780+
except C2paError.ManifestNotFound:
1781+
return None
1782+
1783+
def get_validation_results(self) -> Optional[dict]:
1784+
"""Get the validation results of the manifest store.
1785+
1786+
This method retrieves the full manifest JSON and extracts
1787+
the validation_results object, which contains detailed
1788+
validation information.
1789+
1790+
Returns:
1791+
The validation results as a dictionary containing
1792+
validation details, or None if the validation_results
1793+
field is not present or if no manifest is found or if
1794+
there was an error parsing the JSON.
1795+
"""
1796+
try:
1797+
# Get cached manifest data
1798+
manifest_data = self._get_cached_manifest_data()
1799+
if manifest_data is None:
1800+
return None
1801+
1802+
return manifest_data.get("validation_results")
1803+
except C2paError.ManifestNotFound:
1804+
return None
16461805

16471806
def resource_to_stream(self, uri: str, stream: Any) -> int:
16481807
"""Write a resource to a stream.

0 commit comments

Comments
 (0)