aswf-aces-common 0.1.0rc1__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.
- aswf_aces_common-0.1.0rc1/PKG-INFO +112 -0
- aswf_aces_common-0.1.0rc1/README.md +104 -0
- aswf_aces_common-0.1.0rc1/pyproject.toml +25 -0
- aswf_aces_common-0.1.0rc1/setup.cfg +4 -0
- aswf_aces_common-0.1.0rc1/src/aswf/aces/common/__init__.py +9 -0
- aswf_aces_common-0.1.0rc1/src/aswf/aces/common/constants.py +112 -0
- aswf_aces_common-0.1.0rc1/src/aswf/aces/common/protocols.py +27 -0
- aswf_aces_common-0.1.0rc1/src/aswf/aces/common/types.py +111 -0
- aswf_aces_common-0.1.0rc1/src/aswf_aces_common.egg-info/PKG-INFO +112 -0
- aswf_aces_common-0.1.0rc1/src/aswf_aces_common.egg-info/SOURCES.txt +11 -0
- aswf_aces_common-0.1.0rc1/src/aswf_aces_common.egg-info/dependency_links.txt +1 -0
- aswf_aces_common-0.1.0rc1/src/aswf_aces_common.egg-info/top_level.txt +1 -0
- aswf_aces_common-0.1.0rc1/tests/test_transform_urn.py +107 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aswf-aces-common
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: ACES shared protocols and data types
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# aswf-aces-common
|
|
10
|
+
|
|
11
|
+
Shared protocols and types used across the ACES AMF Python packages.
|
|
12
|
+
|
|
13
|
+
This is a foundation package. Most users should install [aswf-aces-amf-utils](../aces-amf-utils/) or [aswf-aces-amf-lib](../aces-amf-lib/) instead.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install aswf-aces-common
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Exports
|
|
22
|
+
|
|
23
|
+
### `TransformRegistry` (Protocol)
|
|
24
|
+
|
|
25
|
+
Interface for transform ID registries. Implemented by `ACESTransformRegistry` in [aswf-aces-transforms](../aces-transforms/).
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from aswf.aces.common import TransformRegistry
|
|
29
|
+
|
|
30
|
+
class MyRegistry:
|
|
31
|
+
def is_valid_transform_id(self, transform_id: str, *, version: str | None = None) -> bool: ...
|
|
32
|
+
def get_transform_info(self, transform_id: str, *, version: str | None = None) -> dict | None: ...
|
|
33
|
+
def list_transforms(self, *, category: str | None = None, version: str | None = None) -> list[dict]: ...
|
|
34
|
+
def list_versions(self) -> list[str]: ...
|
|
35
|
+
def get_equivalent_id(self, transform_id: str) -> str | None: ...
|
|
36
|
+
def get_equivalent_ids(self, transform_id: str) -> list[str]: ...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `TransformURN` (Dataclass)
|
|
40
|
+
|
|
41
|
+
Parses ACES transform URNs into their component fields per the [Transform ID specification](https://docs.acescentral.com/system-components/transform-id-spec/).
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
urn:ampas:aces:transformId:v{specVersion}:{TransformType}.{Namespace}.{Name}.a{ACESRelease}.v{TransformVersion}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from aswf.aces.common import TransformURN
|
|
49
|
+
|
|
50
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1")
|
|
51
|
+
|
|
52
|
+
urn.raw # "urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1"
|
|
53
|
+
urn.spec_version # "v2.0"
|
|
54
|
+
urn.spec_major_version # 2
|
|
55
|
+
urn.spec_minor_version # 0
|
|
56
|
+
urn.transform_type # "CSC"
|
|
57
|
+
urn.namespace # "Academy"
|
|
58
|
+
urn.name # "ACES_to_ACEScct"
|
|
59
|
+
urn.version_suffix # "a2.v1"
|
|
60
|
+
urn.aces_release_version # 2
|
|
61
|
+
urn.transform_version # "v1"
|
|
62
|
+
urn.is_v1 # False
|
|
63
|
+
urn.is_v2 # True
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The version suffix format varies between ACES versions:
|
|
67
|
+
|
|
68
|
+
| ACES version | Suffix format | Example | `aces_release_version` | `transform_version` |
|
|
69
|
+
|---|---|---|---|---|
|
|
70
|
+
| v2.0 | `a{N}.v{N}` | `a2.v1` | `2` | `"v1"` |
|
|
71
|
+
| v1.x (semver) | `a{N}.{N}.{N}` | `a1.0.3` | `1` | `"0.3"` |
|
|
72
|
+
| v1.x (mixed) | `a{N}.v{N}.{N}` | `a1.v1.0` | `1` | `"v1.0"` |
|
|
73
|
+
|
|
74
|
+
`TransformURN.parse()` returns `None` for strings that are not valid ACES transform URNs (e.g. legacy short names like `ACEScsc.ACES_to_ACEScc.a1.0.3`).
|
|
75
|
+
|
|
76
|
+
#### Edge cases
|
|
77
|
+
|
|
78
|
+
Internal library and utility transforms (`ACESlib`, `ACESutil`) do not include a separate namespace field — their format is `{Type}.{Name}.{VersionSuffix}` rather than `{Type}.{Namespace}.{Name}.{VersionSuffix}`. For these transforms, the `name` field will contain the version suffix and `version_suffix` will be empty, so `aces_release_version` and `transform_version` will return `None`. These transforms are not typically referenced in AMF files.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:ACESlib.Utilities.a1.0.3")
|
|
82
|
+
urn.transform_type # "ACESlib"
|
|
83
|
+
urn.namespace # "Utilities" (actually the name, not a namespace)
|
|
84
|
+
urn.name # "a1.0.3" (actually the version suffix)
|
|
85
|
+
urn.version_suffix # ""
|
|
86
|
+
urn.aces_release_version # None
|
|
87
|
+
urn.transform_version # None
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `TransformInfo` (Dataclass)
|
|
91
|
+
|
|
92
|
+
Metadata for a single ACES transform:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from aswf.aces.common import TransformInfo
|
|
96
|
+
|
|
97
|
+
# Fields: transform_id, user_name, transform_type, aces_version,
|
|
98
|
+
# inverse_transform_id, previous_equivalent_ids
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Constants
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from aswf.aces.common import INPUT_TRANSFORM_TYPES, OUTPUT_TRANSFORM_TYPES
|
|
105
|
+
|
|
106
|
+
INPUT_TRANSFORM_TYPES # frozenset{"IDT", "CSC", "Input"}
|
|
107
|
+
OUTPUT_TRANSFORM_TYPES # frozenset{"ODT", "Output"}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
Apache-2.0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# aswf-aces-common
|
|
2
|
+
|
|
3
|
+
Shared protocols and types used across the ACES AMF Python packages.
|
|
4
|
+
|
|
5
|
+
This is a foundation package. Most users should install [aswf-aces-amf-utils](../aces-amf-utils/) or [aswf-aces-amf-lib](../aces-amf-lib/) instead.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install aswf-aces-common
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Exports
|
|
14
|
+
|
|
15
|
+
### `TransformRegistry` (Protocol)
|
|
16
|
+
|
|
17
|
+
Interface for transform ID registries. Implemented by `ACESTransformRegistry` in [aswf-aces-transforms](../aces-transforms/).
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from aswf.aces.common import TransformRegistry
|
|
21
|
+
|
|
22
|
+
class MyRegistry:
|
|
23
|
+
def is_valid_transform_id(self, transform_id: str, *, version: str | None = None) -> bool: ...
|
|
24
|
+
def get_transform_info(self, transform_id: str, *, version: str | None = None) -> dict | None: ...
|
|
25
|
+
def list_transforms(self, *, category: str | None = None, version: str | None = None) -> list[dict]: ...
|
|
26
|
+
def list_versions(self) -> list[str]: ...
|
|
27
|
+
def get_equivalent_id(self, transform_id: str) -> str | None: ...
|
|
28
|
+
def get_equivalent_ids(self, transform_id: str) -> list[str]: ...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `TransformURN` (Dataclass)
|
|
32
|
+
|
|
33
|
+
Parses ACES transform URNs into their component fields per the [Transform ID specification](https://docs.acescentral.com/system-components/transform-id-spec/).
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
urn:ampas:aces:transformId:v{specVersion}:{TransformType}.{Namespace}.{Name}.a{ACESRelease}.v{TransformVersion}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from aswf.aces.common import TransformURN
|
|
41
|
+
|
|
42
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1")
|
|
43
|
+
|
|
44
|
+
urn.raw # "urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1"
|
|
45
|
+
urn.spec_version # "v2.0"
|
|
46
|
+
urn.spec_major_version # 2
|
|
47
|
+
urn.spec_minor_version # 0
|
|
48
|
+
urn.transform_type # "CSC"
|
|
49
|
+
urn.namespace # "Academy"
|
|
50
|
+
urn.name # "ACES_to_ACEScct"
|
|
51
|
+
urn.version_suffix # "a2.v1"
|
|
52
|
+
urn.aces_release_version # 2
|
|
53
|
+
urn.transform_version # "v1"
|
|
54
|
+
urn.is_v1 # False
|
|
55
|
+
urn.is_v2 # True
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The version suffix format varies between ACES versions:
|
|
59
|
+
|
|
60
|
+
| ACES version | Suffix format | Example | `aces_release_version` | `transform_version` |
|
|
61
|
+
|---|---|---|---|---|
|
|
62
|
+
| v2.0 | `a{N}.v{N}` | `a2.v1` | `2` | `"v1"` |
|
|
63
|
+
| v1.x (semver) | `a{N}.{N}.{N}` | `a1.0.3` | `1` | `"0.3"` |
|
|
64
|
+
| v1.x (mixed) | `a{N}.v{N}.{N}` | `a1.v1.0` | `1` | `"v1.0"` |
|
|
65
|
+
|
|
66
|
+
`TransformURN.parse()` returns `None` for strings that are not valid ACES transform URNs (e.g. legacy short names like `ACEScsc.ACES_to_ACEScc.a1.0.3`).
|
|
67
|
+
|
|
68
|
+
#### Edge cases
|
|
69
|
+
|
|
70
|
+
Internal library and utility transforms (`ACESlib`, `ACESutil`) do not include a separate namespace field — their format is `{Type}.{Name}.{VersionSuffix}` rather than `{Type}.{Namespace}.{Name}.{VersionSuffix}`. For these transforms, the `name` field will contain the version suffix and `version_suffix` will be empty, so `aces_release_version` and `transform_version` will return `None`. These transforms are not typically referenced in AMF files.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:ACESlib.Utilities.a1.0.3")
|
|
74
|
+
urn.transform_type # "ACESlib"
|
|
75
|
+
urn.namespace # "Utilities" (actually the name, not a namespace)
|
|
76
|
+
urn.name # "a1.0.3" (actually the version suffix)
|
|
77
|
+
urn.version_suffix # ""
|
|
78
|
+
urn.aces_release_version # None
|
|
79
|
+
urn.transform_version # None
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `TransformInfo` (Dataclass)
|
|
83
|
+
|
|
84
|
+
Metadata for a single ACES transform:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from aswf.aces.common import TransformInfo
|
|
88
|
+
|
|
89
|
+
# Fields: transform_id, user_name, transform_type, aces_version,
|
|
90
|
+
# inverse_transform_id, previous_equivalent_ids
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Constants
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from aswf.aces.common import INPUT_TRANSFORM_TYPES, OUTPUT_TRANSFORM_TYPES
|
|
97
|
+
|
|
98
|
+
INPUT_TRANSFORM_TYPES # frozenset{"IDT", "CSC", "Input"}
|
|
99
|
+
OUTPUT_TRANSFORM_TYPES # frozenset{"ODT", "Output"}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
Apache-2.0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "aswf-aces-common"
|
|
8
|
+
version = "0.1.0rc1"
|
|
9
|
+
description = "ACES shared protocols and data types"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
|
17
|
+
include = ["aswf.aces.common*"]
|
|
18
|
+
|
|
19
|
+
[[tool.uv.index]]
|
|
20
|
+
name = "pypi"
|
|
21
|
+
url = "https://pypi.org/simple"
|
|
22
|
+
default = true
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright Contributors to the ACES Project.
|
|
3
|
+
"""ACES shared protocols and data types."""
|
|
4
|
+
|
|
5
|
+
from aswf.aces.common.constants import INPUT_TRANSFORM_TYPES, OUTPUT_TRANSFORM_TYPES
|
|
6
|
+
from aswf.aces.common.protocols import TransformRegistry
|
|
7
|
+
from aswf.aces.common.types import TransformInfo, TransformURN
|
|
8
|
+
|
|
9
|
+
__all__ = ["INPUT_TRANSFORM_TYPES", "OUTPUT_TRANSFORM_TYPES", "TransformRegistry", "TransformInfo", "TransformURN"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright Contributors to the ACES Project.
|
|
3
|
+
"""
|
|
4
|
+
Constants derived from the AMF XSD schemas.
|
|
5
|
+
|
|
6
|
+
Transform type prefix constants map to XSD simpleType definitions that
|
|
7
|
+
restrict which transform ID URN prefixes are valid per container type.
|
|
8
|
+
The v1 and v2 schemas define different allowed prefixes.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Transform types considered as input transforms (used by bulk_generator)
|
|
12
|
+
INPUT_TRANSFORM_TYPES: frozenset[str] = frozenset({"IDT", "CSC", "Input"})
|
|
13
|
+
|
|
14
|
+
# Transform types considered as output transforms (used by bulk_generator)
|
|
15
|
+
OUTPUT_TRANSFORM_TYPES: frozenset[str] = frozenset({"ODT", "Output"})
|
|
16
|
+
|
|
17
|
+
# Transform types considered as look transforms
|
|
18
|
+
LOOK_TRANSFORM_TYPES: frozenset[str] = frozenset({"LMT", "Look"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
22
|
+
# AMF v2 XSD — allowed transform ID URN prefixes (v1.5 + v2.0 URNs)
|
|
23
|
+
# Source: packages/aces-amf-lib/src/aswf/aces/amf_lib/data/amf-schema/v2/acesMetadataFile.xsd
|
|
24
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
# tnInputTransform (v2 XSD line 71): IDT, ACEScsc (v1.5) | Input, CSC (v2.0)
|
|
27
|
+
V2_INPUT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"IDT", "ACEScsc", "Input", "CSC"})
|
|
28
|
+
# tnOutputTransform (v2 XSD line 78): RRTODT (v1.5) | Output (v2.0)
|
|
29
|
+
V2_OUTPUT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"RRTODT", "Output"})
|
|
30
|
+
# tnLookTransform (v2 XSD line 64): LMT (v1.5) | Look (v2.0)
|
|
31
|
+
V2_LOOK_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"LMT", "Look"})
|
|
32
|
+
# tnColorSpaceConversionTransform (v2 XSD line 92): ACEScsc (v1.5) | CSC (v2.0)
|
|
33
|
+
V2_CSC_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"ACEScsc", "CSC"})
|
|
34
|
+
# tnInverseOutputTransform (v2 XSD line 85): InvRRTODT (v1.5) | InvOutput (v2.0)
|
|
35
|
+
V2_INVERSE_OUTPUT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"InvRRTODT", "InvOutput"})
|
|
36
|
+
# v1.x only sub-transforms (same in v2 XSD as v1 XSD — no v2.0 equivalents)
|
|
37
|
+
# tnReferenceRenderingTransform (v2 XSD line 100): RRT only
|
|
38
|
+
V2_RRT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"RRT"})
|
|
39
|
+
# tnInverseReferenceRenderingTransform (v2 XSD line 106): InvRRT only
|
|
40
|
+
V2_INVERSE_RRT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"InvRRT"})
|
|
41
|
+
# tnOutputDeviceTransform (v2 XSD line 112): ODT only
|
|
42
|
+
V2_ODT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"ODT"})
|
|
43
|
+
# tnInverseOutputDeviceTransform (v2 XSD line 118): InvODT only
|
|
44
|
+
V2_INVERSE_ODT_TRANSFORM_PREFIXES: frozenset[str] = frozenset({"InvODT"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
48
|
+
# XSD regex patterns for transform IDs, keyed by transform type.
|
|
49
|
+
# Source: packages/aces-amf-lib/src/aswf/aces/amf_lib/data/amf-schema/v2/acesMetadataFile.xsd
|
|
50
|
+
#
|
|
51
|
+
# Each transform type maps to a list of regex patterns (from xs:pattern).
|
|
52
|
+
# A transform ID is valid if it matches ANY pattern for its type.
|
|
53
|
+
# The v2 XSD accepts both v1.5 and v2.0 URN forms.
|
|
54
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
# Transform types that have no XSD pattern (library/utility transforms,
|
|
57
|
+
# not placed in AMF pipeline slots).
|
|
58
|
+
# TODO: Define patterns for these types.
|
|
59
|
+
TRANSFORM_TYPES_WITHOUT_PATTERN: frozenset[str] = frozenset({
|
|
60
|
+
"ACESlib", "ACESutil", "Lib", "InvLMT", "InvLook",
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
TRANSFORM_ID_PATTERNS: dict[str, list[str]] = {
|
|
64
|
+
# tnInputTransform
|
|
65
|
+
"IDT": [
|
|
66
|
+
r"urn:ampas:aces:transformId:v1\.5:IDT\.\S+\.\S+\.a\d+\.v\d+",
|
|
67
|
+
],
|
|
68
|
+
"ACEScsc": [
|
|
69
|
+
r"urn:ampas:aces:transformId:v1\.5:(ACEScsc\.\S+\.\S+\.a\d+\.v\d+|ACEScsc\.Academy\.\S+\.a\d+\.\d+\.\d+)",
|
|
70
|
+
],
|
|
71
|
+
"Input": [
|
|
72
|
+
r"urn:ampas:aces:transformId:v2\.0:Input\.\S+\.\S+\.a\d+\.v\d+",
|
|
73
|
+
],
|
|
74
|
+
"CSC": [
|
|
75
|
+
r"urn:ampas:aces:transformId:v2\.0:CSC\.\S+\.\S+\.a\d+\.v\d+",
|
|
76
|
+
],
|
|
77
|
+
# tnOutputTransform
|
|
78
|
+
"RRTODT": [
|
|
79
|
+
r"urn:ampas:aces:transformId:v1\.5:(RRTODT\.\S+\.\S+\.a\d+\.v\d+|RRTODT\.Academy\.\S+\.a\d+\.\d+\.\d+)",
|
|
80
|
+
],
|
|
81
|
+
"Output": [
|
|
82
|
+
r"urn:ampas:aces:transformId:v2\.0:Output\.\S+\.\S+\.a\d+\.v\d+",
|
|
83
|
+
],
|
|
84
|
+
# tnLookTransform
|
|
85
|
+
"LMT": [
|
|
86
|
+
r"urn:ampas:aces:transformId:v1\.5:(LMT\.\S+\.\S+\.a\d+\.v\d+|LMT\.Academy\.\S+\.a\d+\.v?\d+\.\d+)",
|
|
87
|
+
],
|
|
88
|
+
"Look": [
|
|
89
|
+
r"urn:ampas:aces:transformId:v2\.0:Look\.\S+\.\S+\.a\d+\.v\d+",
|
|
90
|
+
],
|
|
91
|
+
# tnReferenceRenderingTransform / tnInverseReferenceRenderingTransform
|
|
92
|
+
"RRT": [
|
|
93
|
+
r"urn:ampas:aces:transformId:v1\.5:RRT\.a\d+\.\d+\.\d+",
|
|
94
|
+
],
|
|
95
|
+
"InvRRT": [
|
|
96
|
+
r"urn:ampas:aces:transformId:v1\.5:InvRRT\.a\d+\.\d+\.\d+",
|
|
97
|
+
],
|
|
98
|
+
# tnOutputDeviceTransform / tnInverseOutputDeviceTransform
|
|
99
|
+
"ODT": [
|
|
100
|
+
r"urn:ampas:aces:transformId:v1\.5:(ODT\.\S+\.\S+\.a\d+\.v\d+|ODT\.Academy\.\S+\.a\d+\.\d+\.\d+)",
|
|
101
|
+
],
|
|
102
|
+
"InvODT": [
|
|
103
|
+
r"urn:ampas:aces:transformId:v1\.5:(InvODT\.\S+\.\S+\.a\d+\.v\d+|InvODT\.Academy\.\S+\.a\d+\.\d+\.\d+)",
|
|
104
|
+
],
|
|
105
|
+
# tnInverseOutputTransform
|
|
106
|
+
"InvRRTODT": [
|
|
107
|
+
r"urn:ampas:aces:transformId:v1\.5:(InvRRTODT\.\S+\.\S+\.a\d+\.v\d+|InvRRTODT\.Academy\.\S+\.a\d+\.\d+\.\d+)",
|
|
108
|
+
],
|
|
109
|
+
"InvOutput": [
|
|
110
|
+
r"urn:ampas:aces:transformId:v2\.0:InvOutput\.\S+\.\S+\.a\d+\.v\d+",
|
|
111
|
+
],
|
|
112
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright Contributors to the ACES Project.
|
|
3
|
+
"""Protocol definitions for transform registry interfaces."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class TransformRegistry(Protocol):
|
|
12
|
+
"""Protocol for transform ID registries.
|
|
13
|
+
|
|
14
|
+
Implementations provide lookup and listing of known ACES transform URNs.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def is_valid_transform_id(self, transform_id: str, *, version: str | None = None) -> bool: ...
|
|
18
|
+
|
|
19
|
+
def get_transform_info(self, transform_id: str, *, version: str | None = None) -> dict | None: ...
|
|
20
|
+
|
|
21
|
+
def list_transforms(self, *, category: str | None = None, version: str | None = None) -> list[dict]: ...
|
|
22
|
+
|
|
23
|
+
def list_versions(self) -> list[str]: ...
|
|
24
|
+
|
|
25
|
+
def get_equivalent_id(self, transform_id: str) -> str | None: ...
|
|
26
|
+
|
|
27
|
+
def get_equivalent_ids(self, transform_id: str) -> list[str]: ...
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright Contributors to the ACES Project.
|
|
3
|
+
"""Shared ACES data types."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import asdict, dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_TRANSFORM_URN_RE = re.compile(
|
|
12
|
+
r"^urn:ampas:aces:transformId:"
|
|
13
|
+
r"v(\d+)\.(\d+):" # spec version: v2.0, v1.5
|
|
14
|
+
r"([A-Za-z][A-Za-z0-9]*)" # transform type: CSC, Output, IDT, ACEScsc...
|
|
15
|
+
r"\.([^.]+)" # namespace: Academy, Arri, Red...
|
|
16
|
+
r"\.(.+)" # name + version suffix
|
|
17
|
+
r"$"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Matches the trailing version suffix: a2.v1, a1.0.3, a1.v1.0, etc.
|
|
21
|
+
_VERSION_SUFFIX_RE = re.compile(
|
|
22
|
+
r"^(.+)\.(a\d+(?:\.(?:v?\d+))+)$"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class TransformURN:
|
|
27
|
+
"""Parsed ACES transform URN per the Transform ID specification.
|
|
28
|
+
|
|
29
|
+
Format: urn:ampas:aces:transformId:v{specVersion}:{TransformType}.{Namespace}.{Name}.{VersionSuffix}
|
|
30
|
+
|
|
31
|
+
Example (v2.0):
|
|
32
|
+
urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1
|
|
33
|
+
|
|
34
|
+
Example (v1.x):
|
|
35
|
+
urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScct.a1.0.3
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
raw: str
|
|
39
|
+
spec_version: str
|
|
40
|
+
spec_major_version: int
|
|
41
|
+
spec_minor_version: int
|
|
42
|
+
transform_type: str
|
|
43
|
+
namespace: str
|
|
44
|
+
name: str
|
|
45
|
+
version_suffix: str
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def parse(cls, urn: str) -> TransformURN | None:
|
|
49
|
+
"""Parse a URN string. Returns None if not a valid ACES transform URN."""
|
|
50
|
+
m = _TRANSFORM_URN_RE.match(urn)
|
|
51
|
+
if m is None:
|
|
52
|
+
return None
|
|
53
|
+
spec_major, spec_minor, transform_type, namespace, name_and_version = m.groups()
|
|
54
|
+
|
|
55
|
+
# Split the trailing version suffix (e.g. "ACES_to_ACEScct.a2.v1" -> "ACES_to_ACEScct", "a2.v1")
|
|
56
|
+
vs = _VERSION_SUFFIX_RE.match(name_and_version)
|
|
57
|
+
if vs:
|
|
58
|
+
name, version_suffix = vs.groups()
|
|
59
|
+
else:
|
|
60
|
+
name = name_and_version
|
|
61
|
+
version_suffix = ""
|
|
62
|
+
|
|
63
|
+
return cls(
|
|
64
|
+
raw=urn,
|
|
65
|
+
spec_version=f"v{spec_major}.{spec_minor}",
|
|
66
|
+
spec_major_version=int(spec_major),
|
|
67
|
+
spec_minor_version=int(spec_minor),
|
|
68
|
+
transform_type=transform_type,
|
|
69
|
+
namespace=namespace,
|
|
70
|
+
name=name,
|
|
71
|
+
version_suffix=version_suffix,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_v1(self) -> bool:
|
|
76
|
+
return self.spec_major_version == 1
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_v2(self) -> bool:
|
|
80
|
+
return self.spec_major_version == 2
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def aces_release_version(self) -> int | None:
|
|
84
|
+
"""ACES release major version from the suffix (e.g. 2 from 'a2.v1')."""
|
|
85
|
+
m = re.match(r"a(\d+)", self.version_suffix)
|
|
86
|
+
return int(m.group(1)) if m else None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def transform_version(self) -> str | None:
|
|
90
|
+
"""Transform version from the suffix (e.g. 'v1' from 'a2.v1', '0.3' from 'a1.0.3')."""
|
|
91
|
+
m = re.match(r"a\d+\.(.*)", self.version_suffix)
|
|
92
|
+
return m.group(1) if m else None
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
return self.raw
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class TransformInfo:
|
|
100
|
+
"""Information about a single ACES transform."""
|
|
101
|
+
|
|
102
|
+
transform_id: str
|
|
103
|
+
user_name: str
|
|
104
|
+
transform_type: str
|
|
105
|
+
aces_version: str | None = None
|
|
106
|
+
inverse_transform_id: str | None = None
|
|
107
|
+
previous_equivalent_ids: list[str] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict:
|
|
110
|
+
"""Return a plain dict representation."""
|
|
111
|
+
return asdict(self)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aswf-aces-common
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: ACES shared protocols and data types
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# aswf-aces-common
|
|
10
|
+
|
|
11
|
+
Shared protocols and types used across the ACES AMF Python packages.
|
|
12
|
+
|
|
13
|
+
This is a foundation package. Most users should install [aswf-aces-amf-utils](../aces-amf-utils/) or [aswf-aces-amf-lib](../aces-amf-lib/) instead.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install aswf-aces-common
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Exports
|
|
22
|
+
|
|
23
|
+
### `TransformRegistry` (Protocol)
|
|
24
|
+
|
|
25
|
+
Interface for transform ID registries. Implemented by `ACESTransformRegistry` in [aswf-aces-transforms](../aces-transforms/).
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from aswf.aces.common import TransformRegistry
|
|
29
|
+
|
|
30
|
+
class MyRegistry:
|
|
31
|
+
def is_valid_transform_id(self, transform_id: str, *, version: str | None = None) -> bool: ...
|
|
32
|
+
def get_transform_info(self, transform_id: str, *, version: str | None = None) -> dict | None: ...
|
|
33
|
+
def list_transforms(self, *, category: str | None = None, version: str | None = None) -> list[dict]: ...
|
|
34
|
+
def list_versions(self) -> list[str]: ...
|
|
35
|
+
def get_equivalent_id(self, transform_id: str) -> str | None: ...
|
|
36
|
+
def get_equivalent_ids(self, transform_id: str) -> list[str]: ...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `TransformURN` (Dataclass)
|
|
40
|
+
|
|
41
|
+
Parses ACES transform URNs into their component fields per the [Transform ID specification](https://docs.acescentral.com/system-components/transform-id-spec/).
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
urn:ampas:aces:transformId:v{specVersion}:{TransformType}.{Namespace}.{Name}.a{ACESRelease}.v{TransformVersion}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from aswf.aces.common import TransformURN
|
|
49
|
+
|
|
50
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1")
|
|
51
|
+
|
|
52
|
+
urn.raw # "urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1"
|
|
53
|
+
urn.spec_version # "v2.0"
|
|
54
|
+
urn.spec_major_version # 2
|
|
55
|
+
urn.spec_minor_version # 0
|
|
56
|
+
urn.transform_type # "CSC"
|
|
57
|
+
urn.namespace # "Academy"
|
|
58
|
+
urn.name # "ACES_to_ACEScct"
|
|
59
|
+
urn.version_suffix # "a2.v1"
|
|
60
|
+
urn.aces_release_version # 2
|
|
61
|
+
urn.transform_version # "v1"
|
|
62
|
+
urn.is_v1 # False
|
|
63
|
+
urn.is_v2 # True
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The version suffix format varies between ACES versions:
|
|
67
|
+
|
|
68
|
+
| ACES version | Suffix format | Example | `aces_release_version` | `transform_version` |
|
|
69
|
+
|---|---|---|---|---|
|
|
70
|
+
| v2.0 | `a{N}.v{N}` | `a2.v1` | `2` | `"v1"` |
|
|
71
|
+
| v1.x (semver) | `a{N}.{N}.{N}` | `a1.0.3` | `1` | `"0.3"` |
|
|
72
|
+
| v1.x (mixed) | `a{N}.v{N}.{N}` | `a1.v1.0` | `1` | `"v1.0"` |
|
|
73
|
+
|
|
74
|
+
`TransformURN.parse()` returns `None` for strings that are not valid ACES transform URNs (e.g. legacy short names like `ACEScsc.ACES_to_ACEScc.a1.0.3`).
|
|
75
|
+
|
|
76
|
+
#### Edge cases
|
|
77
|
+
|
|
78
|
+
Internal library and utility transforms (`ACESlib`, `ACESutil`) do not include a separate namespace field — their format is `{Type}.{Name}.{VersionSuffix}` rather than `{Type}.{Namespace}.{Name}.{VersionSuffix}`. For these transforms, the `name` field will contain the version suffix and `version_suffix` will be empty, so `aces_release_version` and `transform_version` will return `None`. These transforms are not typically referenced in AMF files.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:ACESlib.Utilities.a1.0.3")
|
|
82
|
+
urn.transform_type # "ACESlib"
|
|
83
|
+
urn.namespace # "Utilities" (actually the name, not a namespace)
|
|
84
|
+
urn.name # "a1.0.3" (actually the version suffix)
|
|
85
|
+
urn.version_suffix # ""
|
|
86
|
+
urn.aces_release_version # None
|
|
87
|
+
urn.transform_version # None
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `TransformInfo` (Dataclass)
|
|
91
|
+
|
|
92
|
+
Metadata for a single ACES transform:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from aswf.aces.common import TransformInfo
|
|
96
|
+
|
|
97
|
+
# Fields: transform_id, user_name, transform_type, aces_version,
|
|
98
|
+
# inverse_transform_id, previous_equivalent_ids
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Constants
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from aswf.aces.common import INPUT_TRANSFORM_TYPES, OUTPUT_TRANSFORM_TYPES
|
|
105
|
+
|
|
106
|
+
INPUT_TRANSFORM_TYPES # frozenset{"IDT", "CSC", "Input"}
|
|
107
|
+
OUTPUT_TRANSFORM_TYPES # frozenset{"ODT", "Output"}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
Apache-2.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/aswf/aces/common/__init__.py
|
|
4
|
+
src/aswf/aces/common/constants.py
|
|
5
|
+
src/aswf/aces/common/protocols.py
|
|
6
|
+
src/aswf/aces/common/types.py
|
|
7
|
+
src/aswf_aces_common.egg-info/PKG-INFO
|
|
8
|
+
src/aswf_aces_common.egg-info/SOURCES.txt
|
|
9
|
+
src/aswf_aces_common.egg-info/dependency_links.txt
|
|
10
|
+
src/aswf_aces_common.egg-info/top_level.txt
|
|
11
|
+
tests/test_transform_urn.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aswf
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Tests for TransformURN parsing."""
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from aswf.aces.common.types import TransformURN
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestTransformURNParse:
|
|
10
|
+
def test_parse_v15_csc(self):
|
|
11
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScct.a1.0.3")
|
|
12
|
+
assert urn is not None
|
|
13
|
+
assert urn.spec_version == "v1.5"
|
|
14
|
+
assert urn.spec_major_version == 1
|
|
15
|
+
assert urn.spec_minor_version == 5
|
|
16
|
+
assert urn.transform_type == "ACEScsc"
|
|
17
|
+
assert urn.namespace == "Academy"
|
|
18
|
+
assert urn.name == "ACES_to_ACEScct"
|
|
19
|
+
assert urn.version_suffix == "a1.0.3"
|
|
20
|
+
assert urn.is_v1 is True
|
|
21
|
+
assert urn.is_v2 is False
|
|
22
|
+
|
|
23
|
+
def test_parse_v20_csc(self):
|
|
24
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1")
|
|
25
|
+
assert urn is not None
|
|
26
|
+
assert urn.spec_version == "v2.0"
|
|
27
|
+
assert urn.spec_major_version == 2
|
|
28
|
+
assert urn.spec_minor_version == 0
|
|
29
|
+
assert urn.transform_type == "CSC"
|
|
30
|
+
assert urn.namespace == "Academy"
|
|
31
|
+
assert urn.name == "ACES_to_ACEScct"
|
|
32
|
+
assert urn.version_suffix == "a2.v1"
|
|
33
|
+
assert urn.is_v1 is False
|
|
34
|
+
assert urn.is_v2 is True
|
|
35
|
+
|
|
36
|
+
def test_parse_v20_output(self):
|
|
37
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:Output.Academy.Rec709-D65_100nit_in_Rec709-D65_BT1886.a2.v1")
|
|
38
|
+
assert urn is not None
|
|
39
|
+
assert urn.transform_type == "Output"
|
|
40
|
+
assert urn.namespace == "Academy"
|
|
41
|
+
assert urn.name == "Rec709-D65_100nit_in_Rec709-D65_BT1886"
|
|
42
|
+
assert urn.version_suffix == "a2.v1"
|
|
43
|
+
|
|
44
|
+
def test_parse_v15_lmt(self):
|
|
45
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:LMT.Academy.ReferenceGamutCompress.a1.v1.0")
|
|
46
|
+
assert urn is not None
|
|
47
|
+
assert urn.transform_type == "LMT"
|
|
48
|
+
assert urn.namespace == "Academy"
|
|
49
|
+
assert urn.name == "ReferenceGamutCompress"
|
|
50
|
+
assert urn.version_suffix == "a1.v1.0"
|
|
51
|
+
assert urn.is_v1 is True
|
|
52
|
+
|
|
53
|
+
def test_parse_v15_idt_manufacturer(self):
|
|
54
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:IDT.ARRI.ARRI-LogC4.a1.v1")
|
|
55
|
+
assert urn is not None
|
|
56
|
+
assert urn.transform_type == "IDT"
|
|
57
|
+
assert urn.namespace == "ARRI"
|
|
58
|
+
assert urn.name == "ARRI-LogC4"
|
|
59
|
+
assert urn.version_suffix == "a1.v1"
|
|
60
|
+
|
|
61
|
+
def test_parse_v20_look(self):
|
|
62
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:Look.Academy.ReferenceGamutCompress.a2.v1")
|
|
63
|
+
assert urn is not None
|
|
64
|
+
assert urn.transform_type == "Look"
|
|
65
|
+
assert urn.name == "ReferenceGamutCompress"
|
|
66
|
+
assert urn.version_suffix == "a2.v1"
|
|
67
|
+
|
|
68
|
+
def test_parse_returns_none_for_invalid(self):
|
|
69
|
+
assert TransformURN.parse("not-a-urn") is None
|
|
70
|
+
assert TransformURN.parse("") is None
|
|
71
|
+
assert TransformURN.parse("urn:ampas:aces:transformId:v1.5:") is None
|
|
72
|
+
|
|
73
|
+
def test_parse_returns_none_for_legacy_short_name(self):
|
|
74
|
+
assert TransformURN.parse("ACEScsc.ACES_to_ACEScc.a1.0.3") is None
|
|
75
|
+
|
|
76
|
+
def test_str_returns_raw(self):
|
|
77
|
+
raw = "urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1"
|
|
78
|
+
urn = TransformURN.parse(raw)
|
|
79
|
+
assert str(urn) == raw
|
|
80
|
+
|
|
81
|
+
def test_aces_release_version_v20(self):
|
|
82
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1")
|
|
83
|
+
assert urn.aces_release_version == 2
|
|
84
|
+
assert urn.transform_version == "v1"
|
|
85
|
+
|
|
86
|
+
def test_aces_release_version_v15_semver(self):
|
|
87
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACES_to_ACEScct.a1.0.3")
|
|
88
|
+
assert urn.aces_release_version == 1
|
|
89
|
+
assert urn.transform_version == "0.3"
|
|
90
|
+
|
|
91
|
+
def test_aces_release_version_v15_mixed(self):
|
|
92
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:LMT.Academy.ReferenceGamutCompress.a1.v1.0")
|
|
93
|
+
assert urn.aces_release_version == 1
|
|
94
|
+
assert urn.transform_version == "v1.0"
|
|
95
|
+
|
|
96
|
+
def test_aces_release_version_empty_suffix(self):
|
|
97
|
+
"""ACESlib transforms may not parse a version suffix."""
|
|
98
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v1.5:ACESlib.ODT_Common.a1.1.0")
|
|
99
|
+
assert urn.aces_release_version is None
|
|
100
|
+
assert urn.transform_version is None
|
|
101
|
+
|
|
102
|
+
def test_frozen(self):
|
|
103
|
+
urn = TransformURN.parse("urn:ampas:aces:transformId:v2.0:CSC.Academy.ACES_to_ACEScct.a2.v1")
|
|
104
|
+
with pytest.raises(AttributeError):
|
|
105
|
+
urn.spec_version = "v3.0"
|
|
106
|
+
|
|
107
|
+
|