Skip to content

CMS Types

cmstypes

PathBase

Bases: BaseModel

Base class for Path object

Attributes:

Name Type Description
path str

The path string

site_id uuid

unique identifier of the site associated with it (optional)

site_name str

name of the site associated with it (optional)

Source code in cascade_cms/cmstypes.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
class PathBase(BaseModel):
    """
    Base class for Path object

    Attributes:
        path (str): The path string
        site_id (uuid): unique identifier of the site associated with it (optional)
        site_name (str): name of the site associated with it (optional)
    """

    model_config = ConfigDict(frozen=True, populate_by_name=True)

    path: str
    site_id: uuid.UUID | None = Field(
        default=None, validation_alias="siteId", serialization_alias="siteId"
    )
    site_name: str | None = Field(
        default=None, validation_alias="siteName", serialization_alias="siteName"
    )

    # Cascade rejects dashed UUIDs for identifiers - serialize as bare hex.
    @field_serializer("site_id")
    def serialize_site_id(self, value: uuid.UUID | None) -> str | None:
        return value.hex if value is not None else None

Path

Bases: PathBase

Represents a Path object that can be called directly. Substitution for TypeIdentifers with asset id Attributes: asset_type (AssetTypes): (Required) the type of the asset

Source code in cascade_cms/cmstypes.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
class Path(PathBase):
    """
    Represents a Path object that can be called directly.
    Substitution for TypeIdentifers with asset id
    Attributes:
        asset_type (AssetTypes): (Required) the type of the asset
    """

    asset_type: AssetTypes

    # Duck-typed surface shared with IdentifierType (logging reads these).
    @property
    def get_id(self) -> None:
        return None

    @property
    def get_type(self) -> str:
        return str(self.asset_type)

    @property
    def get_path(self) -> str:
        return self.path

SimplePayload

Bases: BaseModel

Base class for Cascade CMS Payloads: Payloads are containers for specific Cascade operations that accept inputs

Source code in cascade_cms/cmstypes.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
class SimplePayload(BaseModel):
    """
    Base class for Cascade CMS Payloads:
    Payloads are containers for specific
    Cascade operations that accept inputs
    """

    model_config = ConfigDict(
        populate_by_name=True,
        from_attributes=True,
        validate_assignment=True,
        serialize_by_alias=True,
    )

    @model_serializer  # or @classmethod
    def format_builder(self) -> dict:
        """Wraps proper headers around payload data.

        Args:
            handler (SerializerFunctionWrapHandler): Pydantic validation function

        Returns:
            dict[str,object]: serialized wrapped inSerializerFunctionWrapHandler the inherit class name

            ```python
            {
                "searchInformation"{
                    ...
                }
            }
            ```

        """
        subclass_name = self.__class__.__name__
        fields_info = self.__class__.model_fields

        def dump(value: Any) -> Any:
            # This dict comprehension only aliases top-level keys; a nested
            # BaseModel passed through as-is would serialize under its
            # Python field names instead of its aliases, so recurse.
            if isinstance(value, BaseModel):
                return value.model_dump(by_alias=True)
            if isinstance(value, list):
                return [dump(item) for item in value]
            return value

        aliased = {
            (fields_info[name].serialization_alias or fields_info[name].alias or name): dump(value)
            for name, value in self.__dict__.items()
            if name in fields_info
        }
        return {reformat_name(subclass_name): aliased}

format_builder()

Wraps proper headers around payload data.

Parameters:

Name Type Description Default
handler SerializerFunctionWrapHandler

Pydantic validation function

required

Returns:

Type Description
dict

dict[str,object]: serialized wrapped inSerializerFunctionWrapHandler the inherit class name

dict

```python

dict

{ "searchInformation"{ ... }

dict

}

dict

```

Source code in cascade_cms/cmstypes.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
@model_serializer  # or @classmethod
def format_builder(self) -> dict:
    """Wraps proper headers around payload data.

    Args:
        handler (SerializerFunctionWrapHandler): Pydantic validation function

    Returns:
        dict[str,object]: serialized wrapped inSerializerFunctionWrapHandler the inherit class name

        ```python
        {
            "searchInformation"{
                ...
            }
        }
        ```

    """
    subclass_name = self.__class__.__name__
    fields_info = self.__class__.model_fields

    def dump(value: Any) -> Any:
        # This dict comprehension only aliases top-level keys; a nested
        # BaseModel passed through as-is would serialize under its
        # Python field names instead of its aliases, so recurse.
        if isinstance(value, BaseModel):
            return value.model_dump(by_alias=True)
        if isinstance(value, list):
            return [dump(item) for item in value]
        return value

    aliased = {
        (fields_info[name].serialization_alias or fields_info[name].alias or name): dump(value)
        for name, value in self.__dict__.items()
        if name in fields_info
    }
    return {reformat_name(subclass_name): aliased}

NewAsset

Bases: SimplePayload

Payload for the create operation.

Requires exactly one of site_name/site_id and exactly one of parent_folder_path/parent_folder_id (enforced by _check_required_alternatives). Extra fields are allowed and passed through, since asset-type-specific properties vary per asset_type.

Source code in cascade_cms/cmstypes.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
class NewAsset(SimplePayload):
    """Payload for the `create` operation.

    Requires exactly one of `site_name`/`site_id` and exactly one of
    `parent_folder_path`/`parent_folder_id` (enforced by
    `_check_required_alternatives`). Extra fields are allowed and passed
    through, since asset-type-specific properties vary per `asset_type`.
    """

    model_config = ConfigDict(
        extra="allow",
        validate_by_name=True,
        validate_by_alias=True,
    )

    name: str
    asset_type: AssetTypes
    site_name: str | None = Field(default=None, validation_alias="siteName", serialization_alias="siteName")
    site_id: uuid.UUID | None = Field(default=None, validation_alias="siteId", serialization_alias="siteId")
    parent_folder_path: str | None = Field(default=None, validation_alias="parentFolderPath", serialization_alias="parentFolderPath")
    parent_folder_id: uuid.UUID | None = Field(default=None, validation_alias="parentFolderId", serialization_alias="parentFolderId")

    if TYPE_CHECKING:
        # Typing only, absent at runtime (pydantic's own __init__ runs). Extra
        # fields are allowed and passed through, and the alias spellings
        # (siteName, ...) are accepted, so unknown keywords are Any.
        def __init__(
            self,
            *,
            name: str,
            asset_type: AssetTypes,
            site_name: str | None = None,
            site_id: uuid.UUID | str | None = None,
            parent_folder_path: str | None = None,
            parent_folder_id: uuid.UUID | str | None = None,
            **extra: Any,
        ) -> None: ...

    @field_serializer("site_id", "parent_folder_id")
    def serialize_uuid_as_hex(self, value: uuid.UUID | None) -> str | None:
        return value.hex if value is not None else None

    @model_validator(mode="after")
    def _check_required_alternatives(self) -> Self:
        if (self.site_name is None) == (self.site_id is None):
            raise ValueError("Provide exactly one of site_name or site_id")
        if (self.parent_folder_path is None) == (self.parent_folder_id is None):
            raise ValueError(
                "Provide exactly one of parent_folder_path or parent_folder_id"
            )
        return self

    @model_serializer(mode="wrap")
    def serialize_as_asset(self, handler: SerializerFunctionWrapHandler) -> dict:
        """Cascade expects {"asset": {"<type>": {...}}}"""
        payload_dict = handler(self)
        asset_type = payload_dict.pop("asset_type")
        cleaned = {k: v for k, v in payload_dict.items() if v is not None}
        return {"asset": {asset_type: cleaned}}

    def dump_json(self) -> bytes:
        return new_asset_adapter.dump_json(self, by_alias=True)

serialize_as_asset(handler)

Cascade expects {"asset": {"": {...}}}

Source code in cascade_cms/cmstypes.py
365
366
367
368
369
370
371
@model_serializer(mode="wrap")
def serialize_as_asset(self, handler: SerializerFunctionWrapHandler) -> dict:
    """Cascade expects {"asset": {"<type>": {...}}}"""
    payload_dict = handler(self)
    asset_type = payload_dict.pop("asset_type")
    cleaned = {k: v for k, v in payload_dict.items() if v is not None}
    return {"asset": {asset_type: cleaned}}

IdentifierType

Bases: BaseModel

Resolved reference to a Cascade asset: its UUID, type, and optional path info.

This is the "id-based" counterpart to Path (which references an asset by site + path string instead); see resolve_identifier.

Source code in cascade_cms/cmstypes.py
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
class IdentifierType(BaseModel):
    """Resolved reference to a Cascade asset: its UUID, type, and optional path info.

    This is the "id-based" counterpart to `Path` (which references an
    asset by site + path string instead); see `resolve_identifier`.
    """

    model_config = ConfigDict(
        frozen=True,
        extra="forbid",
        populate_by_name=True,
    )

    identifier: Annotated[uuid.UUID, Field(validation_alias='id', serialization_alias='id')]
    asset_type: Annotated[AssetTypes, Field(default=..., validation_alias="type", serialization_alias="type")]
    recycled: Annotated[bool | None, Field(default=None)] = None
    path: Annotated[PathBase | None, Field(default=None)] = None

    if TYPE_CHECKING:
        # Typing only, absent at runtime: pydantic accepts Cascade's key names
        # (id, type) and the field names, but type checkers cannot see
        # validation_alias.
        @overload
        def __init__(
            self,
            *,
            id: uuid.UUID | str,
            type: AssetTypes,
            recycled: bool | None = None,
            path: PathBase | dict[str, Any] | None = None,
        ) -> None: ...
        @overload
        def __init__(
            self,
            *,
            identifier: uuid.UUID | str,
            asset_type: AssetTypes,
            recycled: bool | None = None,
            path: PathBase | dict[str, Any] | None = None,
        ) -> None: ...
        def __init__(self, **data: Any) -> None: ...

    # Cascade rejects dashed UUIDs for identifiers - serialize as bare hex.
    @field_serializer("identifier")
    def serialize_identifier(self, value: uuid.UUID) -> str:
        return value.hex

    # getters
    @property
    def get_path(self):
        if self.path is not None:
            return self.path.path

    @property
    def get_sitename(self):
        if self.path is not None:
            return self.path.site_name

    @property
    def get_site_id(self):
        # site_id is optional and defaults to None when the response omits it.
        if self.path is not None:
            return self.path.site_id

    @property
    def get_id(self):
        return self.identifier.hex

    @property
    def get_type(self):
        return self.asset_type

    @model_validator(mode="before")
    @classmethod
    def reject_extra_fields(cls, values):
        if isinstance(values, dict):
            if "asset_type" in values and "type" not in values:
                values = {**values, "type": values["asset_type"]}
                values.pop("asset_type")
            if "identifier" in values and "id" not in values:
                values = {**values, "id": values["identifier"]}
                values.pop("identifier")

            allowed = {"id", "type", "recycled", "path"}
            extra = set(values) - allowed
            if extra:
                raise ValueError(
                    f"Identifier payload contains unexpected fields: {sorted(extra)}"
                )
        return values

AssetLogIdentifier

Bases: NamedTuple

Unvalidated (id, raw_type) pair used only to label an edit request/chain for logging.

Not an IdentifierType substitute for URL-building: edit()'s request has no type/id URL segment (see Chain._build_edit_requests), so this only needs to satisfy the duck-typed get_id/get_type/get_path surface that RequestExecutor.log_key and ChainLineBuilder.start read for naming verbose-mode log files/lines.

Source code in cascade_cms/cmstypes.py
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
class AssetLogIdentifier(NamedTuple):
    """Unvalidated (id, raw_type) pair used only to label an `edit`
    request/chain for logging.

    Not an `IdentifierType` substitute for URL-building: `edit()`'s request
    has no type/id URL segment (see `Chain._build_edit_requests`), so this
    only needs to satisfy the duck-typed `get_id`/`get_type`/`get_path`
    surface that `RequestExecutor.log_key` and `ChainLineBuilder.start`
    read for naming verbose-mode log files/lines.
    """

    id: uuid.UUID
    raw_type: str

    @property
    def get_id(self) -> str:
        return self.id.hex

    @property
    def get_type(self) -> str:
        return self.raw_type

    @property
    def get_path(self) -> None:
        return None

ReadOnlyPageConfigError

Bases: AttributeError

Raised on any attempt to modify a PageConfiguration or PageRegion.

Source code in cascade_cms/cmstypes.py
534
535
class ReadOnlyPageConfigError(AttributeError):
    """Raised on any attempt to modify a `PageConfiguration` or `PageRegion`."""

PageRegion

Read-only snapshot of one region of a page configuration.

Cascade ignores region edits sent through a page's edit(); regions can only be changed on the template asset. Any assignment or deletion raises ReadOnlyPageConfigError, and nothing here is ever written back to the asset.

Do not trust Cascade's noBlock/noFormat flags: they have been observed as false on regions with no blockId/blockPath or formatId/ formatPath. Check the ids/paths themselves.

Source code in cascade_cms/cmstypes.py
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
@pydantic_dataclass(config=_PAGE_VIEW_CONFIG)
class PageRegion:
    """Read-only snapshot of one region of a page configuration.

    Cascade ignores region edits sent through a page's `edit()`; regions can
    only be changed on the `template` asset. Any assignment or deletion raises
    `ReadOnlyPageConfigError`, and nothing here is ever written back to the
    asset.

    Do not trust Cascade's `noBlock`/`noFormat` flags: they have been observed
    as `false` on regions with no `blockId`/`blockPath` or `formatId`/
    `formatPath`. Check the ids/paths themselves.
    """

    name: str
    block_id: str | None = Field(
        default=None, validation_alias="blockId", serialization_alias="blockId"
    )
    block_path: str | None = Field(
        default=None, validation_alias="blockPath", serialization_alias="blockPath"
    )
    block_recycled: bool | None = Field(
        default=None, validation_alias="blockRecycled", serialization_alias="blockRecycled"
    )
    no_block: bool | None = Field(
        default=None, validation_alias="noBlock", serialization_alias="noBlock"
    )
    format_id: str | None = Field(
        default=None, validation_alias="formatId", serialization_alias="formatId"
    )
    format_path: str | None = Field(
        default=None, validation_alias="formatPath", serialization_alias="formatPath"
    )
    format_recycled: bool | None = Field(
        default=None, validation_alias="formatRecycled", serialization_alias="formatRecycled"
    )
    no_format: bool | None = Field(
        default=None, validation_alias="noFormat", serialization_alias="noFormat"
    )
    id: str | None = None

PageConfiguration

Read-only snapshot of one page configuration (e.g. ASPX, XML).

Edit configurations on the pageConfigurationSet asset and regions on the template asset. Any assignment or deletion raises ReadOnlyPageConfigError. See PageRegion for why Cascade's own flags must not be trusted.

Source code in cascade_cms/cmstypes.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
@pydantic_dataclass(config=_PAGE_VIEW_CONFIG)
class PageConfiguration:
    """Read-only snapshot of one page configuration (e.g. `ASPX`, `XML`).

    Edit configurations on the `pageConfigurationSet` asset and regions on the
    `template` asset. Any assignment or deletion raises
    `ReadOnlyPageConfigError`. See `PageRegion` for why Cascade's own flags
    must not be trusted.
    """

    name: str
    default_configuration: bool | None = Field(
        default=None, validation_alias="defaultConfiguration", serialization_alias="defaultConfiguration"
    )
    template_id: str | None = Field(
        default=None, validation_alias="templateId", serialization_alias="templateId"
    )
    template_path: str | None = Field(
        default=None, validation_alias="templatePath", serialization_alias="templatePath"
    )
    format_recycled: bool | None = Field(
        default=None, validation_alias="formatRecycled", serialization_alias="formatRecycled"
    )
    page_regions: tuple[PageRegion, ...] = Field(
        default=(), validation_alias="pageRegions", serialization_alias="pageRegions"
    )
    include_xml_declaration: bool | None = Field(
        default=None, validation_alias="includeXMLDeclaration", serialization_alias="includeXMLDeclaration"
    )
    publishable: bool | None = None
    id: str | None = None

workflowInformation

Bases: BaseModel

Response from the readWorkflowInformation operation, describing an asset's active workflow instance and its steps/actions.

Source code in cascade_cms/cmstypes.py
729
730
731
732
733
734
735
736
737
738
739
740
741
742
class workflowInformation(BaseModel):
    """Response from the `readWorkflowInformation` operation, describing an
    asset's active workflow instance and its steps/actions."""

    model_config = ConfigDict(frozen=True)

    related_entity: Annotated[IdentifierType, Field(validation_alias="relatedEntity", serialization_alias="relatedEntity")]
    current_step: Annotated[str, Field(validation_alias="currentStep", serialization_alias="currentStep")]
    ordered_steps: list[WorkflowSteps]
    unordered_steps: list[WorkflowSteps]
    start_date: datetime
    end_date: datetime
    name: str
    workflow_info_id: Annotated[uuid.UUID, Field(validation_alias="workflowInfoId", serialization_alias="workflowInfoId")]

Asset

Dynamic wrapper around a raw Cascade asset JSON payload.

Cascade asset payloads have the shape {"asset": {"<type>": {...}}} with a structure that varies per asset type, so unlike the Pydantic models above this is a thin dict-backed wrapper rather than a fixed schema: _data holds the inner {...} dict by reference, and __setattr__ only enforces that an existing field keeps its Python type when reassigned (it does not validate against a schema). pageConfigurations, if present, is parsed into PageConfiguration models up front (failing fast on a malformed one) for access via get_page_configuration. Those views are read-only snapshots: region and configuration edits are not sent on edit().

Source code in cascade_cms/cmstypes.py
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
class Asset:
    """Dynamic wrapper around a raw Cascade asset JSON payload.

    Cascade asset payloads have the shape `{"asset": {"<type>": {...}}}`
    with a structure that varies per asset type, so unlike the Pydantic
    models above this is a thin dict-backed wrapper rather than a fixed
    schema: `_data` holds the inner `{...}` dict by reference, and
    `__setattr__` only enforces that an existing field keeps its Python
    type when reassigned (it does not validate against a schema).
    `pageConfigurations`, if present, is parsed into `PageConfiguration`
    models up front (failing fast on a malformed one) for access via
    `get_page_configuration`. Those views are read-only snapshots: region
    and configuration edits are not sent on `edit()`.
    """

    _asset_type: str
    _data: dict[str, Any]
    _page_configs: list[PageConfiguration]

    def __init__(self, data: dict):
        object.__setattr__(self, "_asset_type", next(iter(data["asset"].keys())))
        inner: dict[str, Any] = data["asset"][self._asset_type]
        object.__setattr__(self, "_data", inner)

        # Parse pageConfigurations into Pydantic models
        object.__setattr__(self, "_page_configs", self._current_page_views())

    def _current_page_views(self) -> list[PageConfiguration]:
        if "pageConfigurations" not in self._data:
            return []
        return _build_page_views(self._data["pageConfigurations"])

    def __setattr__(self, key: str, value: object) -> None:
        if key.startswith("_"):
            object.__setattr__(self, key, value)
            return
        if key == "pageConfigurations":
            raise ReadOnlyPageConfigError(CONFIGURATION_READ_ONLY_MESSAGE)
        if key in self._data:
            current = self._data[key]
            if type(value) is not type(current):
                raise TypeError(
                    f"Field {key!r} has changed type: "
                    f"expected {type(current).__name__!r}, "
                    f"got {type(value).__name__!r}"
                )
        self._data[key] = value

    @property
    def internal_type(self) -> str:
        """The raw Cascade response wrapper key, lowercased.

        Not a validated `AssetTypes` value — Cascade's wrapper key doesn't
        always match the request-side type (e.g. `"scriptformat"` for a
        `format`/`format_SCRIPT` asset). This is only ever round-tripped back
        into a request body (see `AssetAdapter.dump_json`), which is why no
        normalization happens here; callers that need a validated
        `AssetTypes` must supply/derive one themselves.
        """
        return self._asset_type.lower()

    def get(self, key: str, max_depth=5):
        keys = key.split(".")

        if keys[0] in ("structuredData", "pageConfigurations"):
            if keys[0] == "structuredData":
                message = (
                    "asset.get('structuredData...') returns raw structured data. "
                    "To address one field use asset.get_data_structure(group, "
                    "identifier, direct=True)."
                )
            else:
                message = (
                    "asset.get('pageConfigurations...') returns raw page "
                    "configuration data, which is read-only (Cascade drops "
                    "edits to it). To read a configuration or region use "
                    "asset.get_page_configuration(name, region)."
                )
            warnings.warn(message, stacklevel=2)
        if len(keys) > max_depth:
            raise ValueError(f"{key} exceeds max depth of {max_depth}")

        def recursive(data, index=0):
            if index == len(keys):
                return data
            cur_search_node = keys[index]

            if not isinstance(data, dict):
                raise KeyError(f"Cannot traverse into non-dict value at '{cur_search_node}'")

            if cur_search_node not in data:
                raise KeyError(f"{cur_search_node} not in data")

            return recursive(data[cur_search_node], index=index + 1)
        return recursive(self._data)

    def get_data_structure(
        self: 'Asset',
        group: str | tuple[str, ...],
        identifier: str,
        *,
        direct: bool = False,
    ) -> list[dict[str, Any]] | None:
        """
        Find nodes matching identifier within all instances of a group.
        Returns first match per group instance as a list of node objects by reference.
        Raises KeyError if required fields (identifier, structuredDataNodes) are missing.

        `group` is a str, which is exactly one group identifier (dots are part
        of the identifier, e.g. `"v1.0"`), or a tuple of identifiers, a path
        ending at the group (`("accordion", "row")`). A bare identifier matches
        every group with that identifier, wherever it sits. A tuple matches
        only groups whose chain of enclosing groups ends with those parts, so
        `("accordion", "row")` skips a `row` under `tabs`. A list is rejected
        (TypeError); an empty tuple or an empty/non-str part raises ValueError.

        By default the search inside each matched group is depth-first and
        descends into nested groups, so a group that lacks `identifier`
        itself can match a field of a nested group. Returns None when nothing
        matches.

        With `direct=True` only the matched group's own leaf children are
        checked and nested groups are never entered. A group instance without
        the field directly is skipped, and the result is None when no
        instance has it, as in the default mode.

        Prefer `direct=True` with a tuple path. The default search can return
        a nested group's field instead of the group's own (when the nested
        group comes first, or the group lacks the field), and the result
        count cannot reveal it.
        """
        if isinstance(group, str):
            parts: tuple[str, ...] = (group,)
        elif isinstance(group, tuple):
            parts = group
        else:
            raise TypeError(
                "group must be a str or a tuple of str, "
                f"not {type(group).__name__}"
            )
        if not parts or not all(isinstance(p, str) and p for p in parts):
            raise ValueError(f"Invalid group path {group!r}")

        def find_group(obj, trail=()):
            if isinstance(obj, dict):
                here = trail
                if obj.get("type") == "group":
                    here = trail + (obj.get("identifier"),)
                    if here[-len(parts):] == parts:
                        yield obj
                for value in obj.values():
                    yield from find_group(value, here)
            elif isinstance(obj, list):
                for item in obj:
                    yield from find_group(item, trail)

        def find_in_nodes(nodes):
            for node in nodes:
                try:
                    if (
                        node["identifier"] == identifier
                        and "structuredDataNodes" not in node
                    ):
                        return node
                    if not direct and "structuredDataNodes" in node:
                        result = find_in_nodes(node["structuredDataNodes"])
                        if result:
                            return result
                except KeyError as e:
                    raise KeyError(f"Missing required field in node: {e}")
            return None

        matches = []
        for group_node in find_group(self._data):
            try:
                nodes = group_node["structuredDataNodes"]
                match = find_in_nodes(nodes)
                if match:
                    matches.append(match)
            except KeyError as e:
                raise KeyError(f"Missing required field 'structuredDataNodes' in group node: {e}")

        return matches if matches else None

    @overload
    def get_page_configuration(
        self, configuration_name: str, page_region: None = None
    ) -> PageConfiguration | None: ...
    @overload
    def get_page_configuration(
        self, configuration_name: str, page_region: str
    ) -> PageRegion | None: ...
    def get_page_configuration(
        self, configuration_name: str, page_region: str | None = None
    ) -> PageConfiguration | PageRegion | None:
        """
        Find a page configuration and optionally a specific region within it.

        The returned models are read-only snapshots of the asset's current
        data, rebuilt on every call. Assigning to them (or to
        `pageConfigurations`) raises `ReadOnlyPageConfigError`: Cascade ignores
        region edits sent through `edit()`. Edit configurations on the
        `pageConfigurationSet` asset and regions on the `template` asset.

        Do not trust Cascade's `noBlock`/`noFormat` flags; they can read
        `false` on regions with no block/format ids or paths. A misspelled
        name returns None. Raises pydantic ValidationError if the raw data is
        malformed.

        Args:
            configuration_name: The 'name' of the configuration e.g. 'ASPX', 'XML'
            page_region:        The 'name' of the page region e.g. 'DEFAULT', 'FOOTER' (optional)

        Returns:
            - PageConfiguration object if only configuration_name is provided
            - PageRegion object if page_region is also provided
            - None if either is not found
        """
        object.__setattr__(self, "_page_configs", self._current_page_views())
        try:
            config = next(
                (c for c in self._page_configs if c.name == configuration_name), None
            )
        except (KeyError, AttributeError) as e:
            raise KeyError(f"Missing required field 'name' in PageConfiguration: {e}")

        if config is None:
            return None

        if page_region is None:
            return config

        try:
            region = next((r for r in config.page_regions if r.name == page_region), None)
        except (KeyError, AttributeError) as e:
            raise KeyError(f"Missing required field 'pageRegions' or 'name' in PageRegion: {e}")

        return region

    # Field names Cascade exposes on a site asset for the root container of
    # each asset type. Only asset types confirmed against a real site payload
    # are listed here; unmapped types return None rather than guess.
    _ROOT_CONTAINER_FIELDS: ClassVar[dict[str, str]] = {
        "datadefinition": "rootDataDefinitionContainerId",
        "sharedfield": "rootSharedFieldContainerId",
        "folder": "rootFolderId",
    }

    def root_container_id(self, asset_type: "AssetTypes") -> uuid.UUID | None:
        """Return the root container id for `asset_type` on this site asset.

        `self` must be a `site` asset. Returns None if there's no known root
        field for `asset_type` (see `_ROOT_CONTAINER_FIELDS`).
        """
        field = self._ROOT_CONTAINER_FIELDS.get(asset_type)
        if field is None:
            return None
        value = self._data.get(field)
        if value is None:
            return None
        return uuid.UUID(value)

internal_type property

The raw Cascade response wrapper key, lowercased.

Not a validated AssetTypes value — Cascade's wrapper key doesn't always match the request-side type (e.g. "scriptformat" for a format/format_SCRIPT asset). This is only ever round-tripped back into a request body (see AssetAdapter.dump_json), which is why no normalization happens here; callers that need a validated AssetTypes must supply/derive one themselves.

get_data_structure(group, identifier, *, direct=False)

Find nodes matching identifier within all instances of a group. Returns first match per group instance as a list of node objects by reference. Raises KeyError if required fields (identifier, structuredDataNodes) are missing.

group is a str, which is exactly one group identifier (dots are part of the identifier, e.g. "v1.0"), or a tuple of identifiers, a path ending at the group (("accordion", "row")). A bare identifier matches every group with that identifier, wherever it sits. A tuple matches only groups whose chain of enclosing groups ends with those parts, so ("accordion", "row") skips a row under tabs. A list is rejected (TypeError); an empty tuple or an empty/non-str part raises ValueError.

By default the search inside each matched group is depth-first and descends into nested groups, so a group that lacks identifier itself can match a field of a nested group. Returns None when nothing matches.

With direct=True only the matched group's own leaf children are checked and nested groups are never entered. A group instance without the field directly is skipped, and the result is None when no instance has it, as in the default mode.

Prefer direct=True with a tuple path. The default search can return a nested group's field instead of the group's own (when the nested group comes first, or the group lacks the field), and the result count cannot reveal it.

Source code in cascade_cms/cmstypes.py
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
def get_data_structure(
    self: 'Asset',
    group: str | tuple[str, ...],
    identifier: str,
    *,
    direct: bool = False,
) -> list[dict[str, Any]] | None:
    """
    Find nodes matching identifier within all instances of a group.
    Returns first match per group instance as a list of node objects by reference.
    Raises KeyError if required fields (identifier, structuredDataNodes) are missing.

    `group` is a str, which is exactly one group identifier (dots are part
    of the identifier, e.g. `"v1.0"`), or a tuple of identifiers, a path
    ending at the group (`("accordion", "row")`). A bare identifier matches
    every group with that identifier, wherever it sits. A tuple matches
    only groups whose chain of enclosing groups ends with those parts, so
    `("accordion", "row")` skips a `row` under `tabs`. A list is rejected
    (TypeError); an empty tuple or an empty/non-str part raises ValueError.

    By default the search inside each matched group is depth-first and
    descends into nested groups, so a group that lacks `identifier`
    itself can match a field of a nested group. Returns None when nothing
    matches.

    With `direct=True` only the matched group's own leaf children are
    checked and nested groups are never entered. A group instance without
    the field directly is skipped, and the result is None when no
    instance has it, as in the default mode.

    Prefer `direct=True` with a tuple path. The default search can return
    a nested group's field instead of the group's own (when the nested
    group comes first, or the group lacks the field), and the result
    count cannot reveal it.
    """
    if isinstance(group, str):
        parts: tuple[str, ...] = (group,)
    elif isinstance(group, tuple):
        parts = group
    else:
        raise TypeError(
            "group must be a str or a tuple of str, "
            f"not {type(group).__name__}"
        )
    if not parts or not all(isinstance(p, str) and p for p in parts):
        raise ValueError(f"Invalid group path {group!r}")

    def find_group(obj, trail=()):
        if isinstance(obj, dict):
            here = trail
            if obj.get("type") == "group":
                here = trail + (obj.get("identifier"),)
                if here[-len(parts):] == parts:
                    yield obj
            for value in obj.values():
                yield from find_group(value, here)
        elif isinstance(obj, list):
            for item in obj:
                yield from find_group(item, trail)

    def find_in_nodes(nodes):
        for node in nodes:
            try:
                if (
                    node["identifier"] == identifier
                    and "structuredDataNodes" not in node
                ):
                    return node
                if not direct and "structuredDataNodes" in node:
                    result = find_in_nodes(node["structuredDataNodes"])
                    if result:
                        return result
            except KeyError as e:
                raise KeyError(f"Missing required field in node: {e}")
        return None

    matches = []
    for group_node in find_group(self._data):
        try:
            nodes = group_node["structuredDataNodes"]
            match = find_in_nodes(nodes)
            if match:
                matches.append(match)
        except KeyError as e:
            raise KeyError(f"Missing required field 'structuredDataNodes' in group node: {e}")

    return matches if matches else None

get_page_configuration(configuration_name, page_region=None)

get_page_configuration(configuration_name: str, page_region: None = None) -> PageConfiguration | None
get_page_configuration(configuration_name: str, page_region: str) -> PageRegion | None

Find a page configuration and optionally a specific region within it.

The returned models are read-only snapshots of the asset's current data, rebuilt on every call. Assigning to them (or to pageConfigurations) raises ReadOnlyPageConfigError: Cascade ignores region edits sent through edit(). Edit configurations on the pageConfigurationSet asset and regions on the template asset.

Do not trust Cascade's noBlock/noFormat flags; they can read false on regions with no block/format ids or paths. A misspelled name returns None. Raises pydantic ValidationError if the raw data is malformed.

Parameters:

Name Type Description Default
configuration_name str

The 'name' of the configuration e.g. 'ASPX', 'XML'

required
page_region str | None

The 'name' of the page region e.g. 'DEFAULT', 'FOOTER' (optional)

None

Returns:

Type Description
PageConfiguration | PageRegion | None
  • PageConfiguration object if only configuration_name is provided
PageConfiguration | PageRegion | None
  • PageRegion object if page_region is also provided
PageConfiguration | PageRegion | None
  • None if either is not found
Source code in cascade_cms/cmstypes.py
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
def get_page_configuration(
    self, configuration_name: str, page_region: str | None = None
) -> PageConfiguration | PageRegion | None:
    """
    Find a page configuration and optionally a specific region within it.

    The returned models are read-only snapshots of the asset's current
    data, rebuilt on every call. Assigning to them (or to
    `pageConfigurations`) raises `ReadOnlyPageConfigError`: Cascade ignores
    region edits sent through `edit()`. Edit configurations on the
    `pageConfigurationSet` asset and regions on the `template` asset.

    Do not trust Cascade's `noBlock`/`noFormat` flags; they can read
    `false` on regions with no block/format ids or paths. A misspelled
    name returns None. Raises pydantic ValidationError if the raw data is
    malformed.

    Args:
        configuration_name: The 'name' of the configuration e.g. 'ASPX', 'XML'
        page_region:        The 'name' of the page region e.g. 'DEFAULT', 'FOOTER' (optional)

    Returns:
        - PageConfiguration object if only configuration_name is provided
        - PageRegion object if page_region is also provided
        - None if either is not found
    """
    object.__setattr__(self, "_page_configs", self._current_page_views())
    try:
        config = next(
            (c for c in self._page_configs if c.name == configuration_name), None
        )
    except (KeyError, AttributeError) as e:
        raise KeyError(f"Missing required field 'name' in PageConfiguration: {e}")

    if config is None:
        return None

    if page_region is None:
        return config

    try:
        region = next((r for r in config.page_regions if r.name == page_region), None)
    except (KeyError, AttributeError) as e:
        raise KeyError(f"Missing required field 'pageRegions' or 'name' in PageRegion: {e}")

    return region

root_container_id(asset_type)

Return the root container id for asset_type on this site asset.

self must be a site asset. Returns None if there's no known root field for asset_type (see _ROOT_CONTAINER_FIELDS).

Source code in cascade_cms/cmstypes.py
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
def root_container_id(self, asset_type: "AssetTypes") -> uuid.UUID | None:
    """Return the root container id for `asset_type` on this site asset.

    `self` must be a `site` asset. Returns None if there's no known root
    field for `asset_type` (see `_ROOT_CONTAINER_FIELDS`).
    """
    field = self._ROOT_CONTAINER_FIELDS.get(asset_type)
    if field is None:
        return None
    value = self._data.get(field)
    if value is None:
        return None
    return uuid.UUID(value)

Message

Bases: SimplePayload

A Cascade inbox message, also used as the payload for mark/delete-message operations.

Source code in cascade_cms/cmstypes.py
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
class Message(SimplePayload):
    """A Cascade inbox message, also used as the payload for mark/delete-message operations."""

    model_config = ConfigDict(populate_by_name=True)

    m_from: Annotated[str, Field(validation_alias="from", serialization_alias="from", exclude=True)]
    m_to: Annotated[str, Field(validation_alias="to", serialization_alias="to", exclude=True)]
    m_subject: Annotated[str, Field(validation_alias="subject", serialization_alias="subject", exclude=True)]
    m_date: Annotated[datetime, Field(validation_alias="date", serialization_alias="date", exclude=True)]
    m_id: Annotated[uuid.UUID, Field(validation_alias="id", serialization_alias="id", exclude=True)]
    marked: str = Field("unread", validation_alias="markType", serialization_alias="markType")

    @field_validator("m_date", mode="after")
    @classmethod
    def remove_timezone(cls, dt: datetime) -> datetime:
        return dt.replace(tzinfo=None)

CheckedOutAsset

Bases: BaseModel

Response from the checkOut operation, referencing the new working copy.

Source code in cascade_cms/cmstypes.py
1041
1042
1043
1044
1045
1046
class CheckedOutAsset(BaseModel):
    """Response from the `checkOut` operation, referencing the new working copy."""

    model_config = ConfigDict(frozen=True)

    working_copy_identifier: IdentifierType = Field(validation_alias="workingCopyIdentifier", serialization_alias="workingCopyIdentifier")

ListElements

Bases: BaseModel

Response container for list-shaped endpoints (search, listSites, listMessages, readAudits, listSubscribers), whose JSON key varies by endpoint but is always aliased into elements via AliasChoices.

Source code in cascade_cms/cmstypes.py
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
class ListElements(BaseModel):
    """Response container for list-shaped endpoints (search, listSites, listMessages,
    readAudits, listSubscribers), whose JSON key varies by endpoint but is always
    aliased into `elements` via `AliasChoices`."""

    model_config = ConfigDict(frozen=True)
    elements: list[IdentifierType | Message | Audit] = Field(
        validation_alias=AliasChoices(
            "preferences",
            "matches",
            "messages",
            "relationships",
            "sites",
            "audits",
        )
    )

    @property
    def flat(self) -> list[IdentifierType | Message | Audit]:
        return self.elements

CascadeError

Bases: BaseModel

Represents a Cascade API-level failure response ({"success": false, "message": ...}).

Source code in cascade_cms/cmstypes.py
1071
1072
1073
1074
1075
1076
class CascadeError(BaseModel):
    """Represents a Cascade API-level failure response (`{"success": false, "message": ...}`)."""

    model_config = ConfigDict(frozen=True, extra='forbid')
    success: Literal[False] = False
    message: str = ""

CascadeSuccess

Bases: BaseModel

Represents a Cascade API-level success response with no further data ({"success": true}).

Source code in cascade_cms/cmstypes.py
1079
1080
1081
1082
1083
class CascadeSuccess(BaseModel):
    """Represents a Cascade API-level success response with no further data (`{"success": true}`)."""

    model_config = ConfigDict(frozen=True, extra='forbid')
    success: Literal[True] = True

SearchInformation

Bases: SimplePayload

Payload for the search operation.

Source code in cascade_cms/cmstypes.py
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
class SearchInformation(SimplePayload):
    """Payload for the `search` operation."""

    site_name: str = Field(validation_alias="siteName", serialization_alias="siteName")
    search_terms: str = Field(validation_alias="searchTerms", serialization_alias="searchTerms")
    search_fields: list[FieldsSearchTypes] | list[Literal[""]] = Field(
        validation_alias="searchFields", serialization_alias="searchFields",
        default_factory=lambda: [cast(Literal[""], "")]
    )
    search_types: list[AssetTypes] | list[Literal[""]] = Field(
        validation_alias="searchTypes", serialization_alias="searchTypes",
        default_factory=lambda: [cast(Literal[""], "")]
    )

preference

Bases: SimplePayload

Payload for the editPreference operation (a single user preference name/value).

Source code in cascade_cms/cmstypes.py
1104
1105
1106
1107
1108
class preference(SimplePayload):
    """Payload for the `editPreference` operation (a single user preference name/value)."""

    name: str
    value: str | None

deleteParameters

Bases: SimplePayload

Payload for the delete operation.

Source code in cascade_cms/cmstypes.py
1111
1112
1113
1114
1115
1116
class deleteParameters(SimplePayload):
    """Payload for the `delete` operation."""

    do_workflow: bool = Field(validation_alias="doWorkflow", serialization_alias="doWorkflow")
    destinations_identifiers: list[IdentifierType] = Field(validation_alias="destinations", serialization_alias="destinations")
    unpublish: bool = True

copyParameters

Bases: SimplePayload

Payload for the copy operation.

Source code in cascade_cms/cmstypes.py
1119
1120
1121
1122
1123
1124
1125
1126
class copyParameters(SimplePayload):
    """Payload for the `copy` operation."""

    do_workflow: Annotated[bool, Field(validation_alias="doWorkflow", serialization_alias="doWorkflow")]
    new_name: Annotated[str, Field(default=..., validation_alias="newName", serialization_alias="newName")]
    destination_container_identifier: Annotated[
        IdentifierType, Field(validation_alias="destinationContainerIdentifier", serialization_alias="destinationContainerIdentifier")  # required
    ]

moveParameters

Bases: SimplePayload

Payload for the move operation.

Source code in cascade_cms/cmstypes.py
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
class moveParameters(SimplePayload):
    """Payload for the `move` operation."""

    destinations: list[IdentifierType]
    do_workflow: bool = Field(validation_alias="doWorkflow", serialization_alias="doWorkflow")
    destination_container_identifier: IdentifierType = Field(
        validation_alias="destinationContainerIdentifier", serialization_alias="destinationContainerIdentifier"
    )
    new_name: str = Field(default="", validation_alias="newName", serialization_alias="newName")  # empty new name means no rename
    unpublish: bool = True

publishInformation

Bases: SimplePayload

Payload for the publish operation.

Source code in cascade_cms/cmstypes.py
1141
1142
1143
1144
class publishInformation(SimplePayload):
    """Payload for the `publish` operation."""

    unpublish: bool = True

Comment

Bases: SimplePayload

Payload for the checkIn operation (a check-in comment).

Source code in cascade_cms/cmstypes.py
1147
1148
1149
1150
class Comment(SimplePayload):
    """Payload for the `checkIn` operation (a check-in comment)."""

    comment: str

SiteCopyParameter

Bases: SimplePayload

Payload for the siteCopy operation.

Source code in cascade_cms/cmstypes.py
1153
1154
1155
1156
1157
class SiteCopyParameter(SimplePayload):
    """Payload for the `siteCopy` operation."""

    original_sitename: str | IdentifierType = Field(validation_alias="originalSiteName", serialization_alias="originalSiteName")
    new_sitename: str = Field(validation_alias="newSiteName", serialization_alias="newSiteName")

workflowTransitionInformation

Bases: SimplePayload

Payload for the performWorkflowTransition operation.

Source code in cascade_cms/cmstypes.py
1160
1161
1162
1163
1164
1165
class workflowTransitionInformation(SimplePayload):
    """Payload for the `performWorkflowTransition` operation."""

    workflow_identifier: Annotated[uuid.UUID, Field(validation_alias="workflowId", serialization_alias="workflowId")]
    action_identifier: Annotated[str, Field(validation_alias="actionIdentifier", serialization_alias="actionIdentifier")]
    transition_comment: str | None = Field(validation_alias="transitionComment", serialization_alias="transitionComment")

auditParameters

Bases: SimplePayload

Payload for the readAudits operation.

Source code in cascade_cms/cmstypes.py
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
class auditParameters(SimplePayload):
    """Payload for the `readAudits` operation."""

    audit_type: AuditTypes = Field(validation_alias="auditType", serialization_alias="auditType")
    by_username: str | None = Field(default=None, validation_alias="username", serialization_alias="username")
    by_group: str | None = Field(default=None, validation_alias="groupname", serialization_alias="groupname")
    by_role: str | None = Field(default=None, validation_alias="rolename", serialization_alias="rolename")
    start_date: datetime | None = Field(default=None, validation_alias="startDate", serialization_alias="startDate")
    end_date: datetime | None = Field(default=None, validation_alias="endDate", serialization_alias="endDate")

    # Cascade needs at least one of username / groupname / rolename
    @model_validator(mode="after")
    def requires_username_group_or_role(self) -> Self:
        if not (self.by_username or self.by_group or self.by_role):
            raise ValueError("Provide at least one of username, groupname, or rolename.")
        return self

    """
    def toJson(self) -> str:
        return self.model_dump_json(
            by_alias=True,
            exclude_none=True,
        )
    """

AssetAdapter

Drop-in counterpart to TypeAdapter for Asset objects.

Mirrors TypeAdapter's validate_json / dump_json interface so callers never need isinstance checks to decide how to serialize/deserialize.

Source code in cascade_cms/cmstypes.py
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
class AssetAdapter:
    """Drop-in counterpart to TypeAdapter for Asset objects.

    Mirrors TypeAdapter's validate_json / dump_json interface so callers
    never need isinstance checks to decide how to serialize/deserialize.
    """

    def validate_json(self, json_str: bytes | str) -> Asset:
        return Asset(json.loads(json_str))

    def dump_json(self, asset: Asset) -> bytes:
        # `_data` is what gets serialized. The `_page_configs` models are
        # read-only snapshots and are never written back. The raw dicts keep
        # every field the models don't parse (templateId, blockId,
        # formatId, ...), so round-tripping preserves them.
        data = {**asset._data}
        reconstructed = {"asset": {asset._asset_type: data}}
        return json.dumps(reconstructed).encode()

ResponseParser

Bases: BaseModel

Parses a raw response body, trying CascadeError first and falling back to serializer on the expected success shape.

_content holds the parsed result (either a CascadeError or a T).

Source code in cascade_cms/cmstypes.py
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
class ResponseParser[T](BaseModel):
    """Parses a raw response body, trying `CascadeError` first and falling
    back to `serializer` on the expected success shape.

    `_content` holds the parsed result (either a `CascadeError` or a `T`).
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    serializer: TypeAdapter[Any] | AssetAdapter
    _content: T | CascadeError | None = PrivateAttr(default=None)

    def __init__(
        self,
        raw: bytes,
        serializer: TypeAdapter[Any] | AssetAdapter,
        **kwargs,
    ):
        super().__init__(serializer=serializer, **kwargs)
        try:
            self._content = CascadeError.model_validate_json(raw)
        except ValidationError:
            self._content = self.serializer.validate_json(raw)  # type: ignore[assignment]

reformat_name(class_name)

Lowercase the first letter of a class name (e.g. "NewAsset" -> "newAsset").

Source code in cascade_cms/cmstypes.py
181
182
183
184
185
def reformat_name(class_name: str):
    """Lowercase the first letter of a class name (e.g. "NewAsset" -> "newAsset")."""
    if class_name[0].isupper():
        return class_name[0].lower() + class_name[1:]
    return class_name

set_checkedout(key)

Toggle a checkout-segment key in the local checkout ledger.

Called once per checkIn/checkOut operation queued, so calling it twice for the same key (once on checkOut, once on the matching checkIn) flips it back out again rather than accumulating duplicates.

Source code in cascade_cms/cmstypes.py
188
189
190
191
192
193
194
195
196
197
198
def set_checkedout(key: str):
    """Toggle a checkout-segment key in the local checkout ledger.

    Called once per checkIn/checkOut operation queued, so calling it twice
    for the same key (once on checkOut, once on the matching checkIn) flips
    it back out again rather than accumulating duplicates.
    """
    if key in ALL_CHECKOUT_ASSETS:
        ALL_CHECKOUT_ASSETS.discard(key)
    else:
        ALL_CHECKOUT_ASSETS.add(key)

resolve_identifier(identifier)

Returns the URL path segments (after the operation name) identifying this asset.

IdentifierType resolves to (asset_type, id). Path resolves to (asset_type, siteName, path), matching the REST endpoint shape .../{operation_name}/{asset_type}/{siteName}/{path}.

Source code in cascade_cms/cmstypes.py
474
475
476
477
478
479
480
481
482
483
484
485
def resolve_identifier(identifier: "IdentifierType | Path") -> tuple[str, ...]:
    """Returns the URL path segments (after the operation name) identifying this asset.

    IdentifierType resolves to (asset_type, id). Path resolves to
    (asset_type, siteName, path), matching the REST endpoint shape
    `.../{operation_name}/{asset_type}/{siteName}/{path}`.
    """
    if isinstance(identifier, IdentifierType):
        return (str(identifier.get_type), str(identifier.get_id))
    if identifier.site_name is None:
        raise ValueError("Path identifiers require site_name to build the request URL")
    return (str(identifier.asset_type), identifier.site_name, identifier.path)

edit_log_identifier_from_asset(asset)

Build the (id, raw_type) label edit() reports itself under in logs.

Uses Asset.internal_type (the raw response wrapper key) rather than a validated AssetTypes value, since this is never used to build a request URL — only to name/label verbose-mode logs.

Source code in cascade_cms/cmstypes.py
515
516
517
518
519
520
521
522
523
524
525
def edit_log_identifier_from_asset(asset: "Asset") -> AssetLogIdentifier:
    """Build the (id, raw_type) label `edit()` reports itself under in logs.

    Uses `Asset.internal_type` (the raw response wrapper key) rather than a
    validated `AssetTypes` value, since this is never used to build a
    request URL — only to name/label verbose-mode logs.
    """
    return AssetLogIdentifier(
        id=uuid.UUID(asset.get("id")),
        raw_type=asset.internal_type,
    )

serialize_payload(payload)

Serialize a request payload to JSON bytes, dispatching by payload type.

Source code in cascade_cms/cmstypes.py
1265
1266
1267
1268
1269
1270
1271
def serialize_payload(payload: Payloads) -> bytes:
    """Serialize a request payload to JSON bytes, dispatching by payload type."""
    if isinstance(payload, Asset):
        return asset_adapter.dump_json(payload)
    elif isinstance(payload, NewAsset):
        return new_asset_adapter.dump_json(payload, by_alias=True)
    return simple_payload_adapter.dump_json(payload)

parse_assets(raw)

Parse a read response body into an Asset.

Source code in cascade_cms/cmstypes.py
1305
1306
1307
1308
def parse_assets(raw: bytes) -> ResponseParser[Asset]:
    """Parse a `read` response body into an `Asset`."""
    a: ResponseParser[Asset] = ResponseParser(raw=raw, serializer=asset_adapter)
    return a

parse_list_elements(raw)

Parse a list-shaped response body (search, listSites, etc.) into ListElements.

Source code in cascade_cms/cmstypes.py
1311
1312
1313
1314
def parse_list_elements(raw: bytes) -> ResponseParser[ListElements]:
    """Parse a list-shaped response body (search, listSites, etc.) into `ListElements`."""
    a: ResponseParser[ListElements] = ResponseParser(raw, serializer=list_element_adapter)
    return a

parse_payloads(raw)

Parse a generic response body into the appropriate SimplePayload subclass.

Source code in cascade_cms/cmstypes.py
1317
1318
1319
def parse_payloads(raw: bytes) -> ResponseParser[SimplePayload]:
    """Parse a generic response body into the appropriate `SimplePayload` subclass."""
    return ResponseParser(raw=raw, serializer=simple_payload_adapter)

parse_create_asset(raw, pass_type)

Parse a create response, rebuilding an IdentifierType from createdAssetId.

Cascade's create response only returns the new asset's id, not its type, so pass_type (the asset_type from the original create payload, bound via functools.partial in Operations.create) is injected to reconstruct a full IdentifierType.

Source code in cascade_cms/cmstypes.py
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
def parse_create_asset(raw: bytes, pass_type: str) -> ResponseParser[IdentifierType]:
    """Parse a `create` response, rebuilding an `IdentifierType` from `createdAssetId`.

    Cascade's create response only returns the new asset's id, not its
    type, so `pass_type` (the `asset_type` from the original create
    payload, bound via `functools.partial` in `Operations.create`) is
    injected to reconstruct a full `IdentifierType`.
    """
    data = json.loads(raw)
    if data.get("createdAssetId") is not None: # we know that the creation succeeded
        identifier_payload = {"id": data["createdAssetId"], "type": pass_type}

        return ResponseParser(
            json.dumps(identifier_payload).encode(),
            serializer=identifier_type_adapter,
        )
    return ResponseParser( # `createdAssetId` does NOT exist we know that its most likely a error
        raw,
        serializer=identifier_type_adapter,
    )

parse_access_rights(raw)

Parse a readAccessRights response body.

Source code in cascade_cms/cmstypes.py
1344
1345
1346
def parse_access_rights(raw: bytes) -> ResponseParser[accessRightsInformationPayload]:
    """Parse a `readAccessRights` response body."""
    return ResponseParser(raw=raw, serializer=access_rights_adapter)

parse_workflow_settings(raw)

Parse a readWorkflowSettings response body.

Source code in cascade_cms/cmstypes.py
1349
1350
1351
def parse_workflow_settings(raw: bytes) -> ResponseParser[workflowSettingsPayload]:
    """Parse a `readWorkflowSettings` response body."""
    return ResponseParser(raw=raw, serializer=workflow_settings_adapter)

parse_checked_out_asset(raw)

Parse a checkOut response body.

Source code in cascade_cms/cmstypes.py
1354
1355
1356
def parse_checked_out_asset(raw: bytes) -> ResponseParser[CheckedOutAsset]:
    """Parse a `checkOut` response body."""
    return ResponseParser(raw=raw, serializer=checked_out_adapter)

parse_workflow_information(raw)

Parse a readWorkflowInformation response body.

Source code in cascade_cms/cmstypes.py
1359
1360
1361
def parse_workflow_information(raw: bytes) -> ResponseParser[workflowInformation]:
    """Parse a `readWorkflowInformation` response body."""
    return ResponseParser(raw=raw, serializer=workflow_info_adapter)

parse_success(raw)

Parse a bare {"success": true} response body from a write operation.

Source code in cascade_cms/cmstypes.py
1364
1365
1366
def parse_success(raw: bytes) -> ResponseParser[CascadeSuccess]:
    """Parse a bare `{"success": true}` response body from a write operation."""
    return ResponseParser(raw=raw, serializer=cascade_success_adapter)