Skip to content

DomoPage

DomoPage

Page classes and functionality.

This module provides comprehensive page management functionality for Domo pages, organized using the subentity composition pattern:

  • exceptions: Page-related exception classes
  • core: Main DomoPage entity classes (DomoPage_Default, DomoPage factory)
  • pages: DomoPages collection and hierarchy operations
  • layout: Layout and content management subentity (DomoPageLayout)

Classes:

Name Description
DomoPage

Factory class for page operations

DomoPage_Default

Default page implementation

DomoPages

Collection class for managing multiple pages

DomoPageAccessController

Access control subentity

DomoPageLayout

Layout and content management subentity

Example

Basic page usage with subentities:

>>> page = await DomoPage.get_by_id(page_id="123", auth=auth)
>>> access_list = await page.Access.get_accesslist()
>>> owners = await page.Access.get_owners()

>>> # Get cards
>>> cards = await page.Layout.get_cards()
>>> # Update layout
>>> await page.Layout.update(body={...})
DomoLineageLink_Page(
    auth: DomoAuth,
    id: str,
    entity: Any,
    _type: str | None = None,
    dependencies: list[DomoLineage_Link] = list(),
    dependents: list[DomoLineage_Link] = list(),
)

Bases: DomoLineage_Link

get_entity(
    debug_api: bool = False,
    session: AsyncClient | None = None,
    *,
    context=None
)

Get the entity associated with this lineage link.

Source code in src/crew_dcs/classes/DomoPage/lineage.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
async def get_entity(
    self,
    debug_api: bool = False,
    session: httpx.AsyncClient | None = None,
    *,
    context=None,
):
    """Get the entity associated with this lineage link."""
    from .core import DomoPage

    if context is None:
        context = RouteContext(
            session=session,
            debug_api=debug_api,
        )

    return await DomoPage.get_by_id(
        page_id=self.id,
        auth=self.auth,
        context=context,
    )

DomoLineage_Page dataclass

DomoLineage_Page(
    auth: DomoAuth,
    parent: Any = None,
    lineage: list[DomoLineage_Link] = list(),
    immediate_dependencies: list[DomoLineage_Link] = list(),
    immediate_dependents: list[DomoLineage_Link] = list(),
    downstream_lineage: list[DomoLineage_Link] = list(),
    cards: list[Any] = None,
)

Bases: DomoLineage

Lineage handler for page entities.

The parent is the page entity this lineage is based off of (not a dependency). Pages have cards as their immediate dependencies.

get async

get(
    session: AsyncClient = None,
    debug_api: bool = False,
    return_raw: bool = False,
    parent_auth: DomoAuth = None,
    parent_auth_retrieval_fn: (
        Callable[[str], Any] | None
    ) = None,
    max_depth: int = 100,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> list[DomoLineage_Link]

Get lineage for a page by traversing card lineage.

Pages do not have direct datacenter lineage. Instead, we load cards from the page layout and then fetch each card's lineage. Each card is added to the page lineage as a LineageLink with dependencies and dependents referenced from the card's lineage root node.

Source code in src/crew_dcs/classes/DomoPage/lineage.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
async def get(
    self,
    session: httpx.AsyncClient = None,
    debug_api: bool = False,
    return_raw: bool = False,
    parent_auth: DomoAuth = None,
    parent_auth_retrieval_fn: Callable[[str], Any] | None = None,
    max_depth: int = 100,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> list[DomoLineage_Link]:
    """Get lineage for a page by traversing card lineage.

    Pages do not have direct datacenter lineage. Instead, we load cards
    from the page layout and then fetch each card's lineage. Each card
    is added to the page lineage as a LineageLink with dependencies and
    dependents referenced from the card's lineage root node.
    """
    context = RouteContext.build_context(
        context=context,
        session=session,
        debug_api=debug_api,
        **context_kwargs,
    )

    if not self.parent:
        raise ClassError(
            message="Parent must be set. Use from_parent() to create lineage with a parent.",
            cls_instance=self,
        )

    cache_key = self._build_cache_key(
        parent_auth=parent_auth,
        parent_auth_retrieval_fn=parent_auth_retrieval_fn,
        return_raw=return_raw,
    )

    if self.lineage and self._cached_lineage_params == cache_key:
        await logger.debug(
            f"DomoLineage_Page.get: returning cached lineage (len={len(self.lineage)})"
        )
        return list(self.lineage)

    cards = await self.parent.Layout.get_cards(
        context=context,
        parent_auth_retrieval_fn=parent_auth_retrieval_fn,
    )

    self.cards = cards

    lineage_cache = self._lineage_cache
    lineage_groups: list[list[DomoLineage_Link]] = []
    card_links: list[DomoLineage_Link] = []

    for card in cards:
        card_lineage = await card.Lineage.get(
            session=session,
            debug_api=debug_api,
            return_raw=return_raw,
            parent_auth=parent_auth,
            parent_auth_retrieval_fn=parent_auth_retrieval_fn,
            max_depth=max_depth,
            lineage_cache=lineage_cache,
            context=context,
        )

        card_root = next(
            (
                link
                for link in card_lineage
                if str(link.id) == str(card.id) and link.type == "CARD"
            ),
            None,
        )

        card_link = DomoLineageLink_Card(
            id=str(card.id),
            auth=card.auth,
            entity=card,
            _type=None,
            dependencies=card_root.dependencies if card_root else [],
            dependents=card_root.dependents if card_root else [],
        )
        card_links.append(card_link)
        lineage_groups.append(card_lineage)

    page_link = self._build_root_link(
        dependencies=card_links,
        dependents=[],
    )

    merged_lineage = self._merge_lineage_links(
        [page_link],
        card_links,
        *lineage_groups,
    )

    self.immediate_dependencies = card_links
    self.immediate_dependents = []
    self.lineage = [page_link] + [
        link for link in merged_lineage if link is not page_link
    ]

    # Connect subscriber page → publisher page if a publisher PAGE
    # exists in the merged lineage (it comes from card federation chains).
    for link in self.lineage:
        if link.type == "PAGE" and str(link.id) != str(page_link.id):
            self._add_direct_link(page_link, link)
            break

    # Connect subscriber page → subscription so the diagram shows the
    # page directly linked to the subscription entity.
    for link in self.lineage:
        if link.type == "SUBSCRIPTION":
            self._add_direct_link(page_link, link)
            break

    self._cached_lineage_params = cache_key

    return list(self.lineage)

get_downstream async

get_downstream(
    session: AsyncClient = None,
    debug_api: bool = False,
    return_raw: bool = False,
    max_depth: int = 1,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> list[DomoLineage_Link]

Get downstream lineage for a page.

A page's downstream dependents are publications that include this page. Also includes any datacenter API results.

Source code in src/crew_dcs/classes/DomoPage/lineage.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
async def get_downstream(
    self,
    session: httpx.AsyncClient = None,
    debug_api: bool = False,
    return_raw: bool = False,
    max_depth: int = 1,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> list[DomoLineage_Link]:
    """Get downstream lineage for a page.

    A page's downstream dependents are publications that include this page.
    Also includes any datacenter API results.
    """
    from ..DomoEverywhere.lineage import DomoLineageLink_Publication

    context = RouteContext.build_context(
        context=context,
        session=session,
        debug_api=debug_api,
        **context_kwargs,
    )

    # Try to find publications that include this page
    publication_links: list[DomoLineage_Link] = []
    try:
        # Check if the page is published via the Federation system
        if hasattr(self.parent, "Federation") and self.parent.Federation:
            federation = self.parent.Federation
            if hasattr(federation, "is_published") and federation.is_published:
                # Get the subscription/publication chain
                subscription = getattr(federation, "subscription", None)
                if subscription:
                    publication = getattr(federation, "parent_publication", None)
                    if publication:
                        pub_link = DomoLineageLink_Publication(
                            id=str(publication.id),
                            auth=self.auth,
                            entity=publication,
                            _type="PUBLICATION",
                            dependencies=[],
                            dependents=[],
                        )
                        publication_links.append(pub_link)
    except (AttributeError, ValueError, TypeError) as exc:
        await logger.warning(
            f"DomoLineage_Page.get_downstream: publication discovery failed for page {self.parent.id}: {exc}"
        )

    # Also try the base class datacenter API
    try:
        base_downstream = await super().get_downstream(
            session=session,
            debug_api=debug_api,
            return_raw=return_raw,
            max_depth=max_depth,
            context=context,
            **context_kwargs,
        )
    except (httpx.HTTPStatusError, ValueError) as exc:
        await logger.warning(
            f"DomoLineage_Page.get_downstream: base downstream lookup failed for page {self.parent.id}: {exc}"
        )
        base_downstream = []

    # Merge publication links with base downstream results
    all_links = publication_links + [
        link for link in base_downstream if link not in publication_links
    ]

    self.immediate_dependents = publication_links
    self.downstream_lineage = all_links

    return all_links

DomoPage dataclass

DomoPage(
    auth: DomoAuth,
    id: int,
    raw: dict,
    Lineage: DomoLineage | None = None,
    title: str | None = None,
    top_page_id: int | None = None,
    parent_page_id: int | None = None,
    is_locked: bool | None = None,
    collections: list[dict] = list(),
    custom_attributes: dict[str, Any] = dict(),
    parent_page: dict[str, Any] | None = None,
    top_page: dict[str, Any] | None = None,
    children: list[Any] = list(),
    Access: DomoPageAccessController | None = None,
    Layout: DomoPageLayout | None = None,
)

Bases: DomoPage_Default

DomoPage entity with federation support via FederationContext.

create async classmethod

create(
    auth: DomoAuth,
    title: str,
    parent_page_id: int = 0,
    *,
    return_raw: bool = False,
    context: RouteContext | None = None,
    **context_kwargs
)

Create a new page (dashboard) and return the hydrated DomoPage.

For a full dashboard (page + cards + layout) use dashboard_builder.create_dashboard; this creates the page shell.

Parameters:

Name Type Description Default
auth DomoAuth

DomoAuth instance.

required
title str

Page title.

required
parent_page_id int

Parent page id (0 for a top-level page).

0
return_raw bool

Return the raw create response instead of a DomoPage.

False
context RouteContext | None

Optional RouteContext.

None
Source code in src/crew_dcs/classes/DomoPage/core.py
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
@classmethod
async def create(
    cls,
    auth: DomoAuth,
    title: str,
    parent_page_id: int = 0,
    *,
    return_raw: bool = False,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Create a new page (dashboard) and return the hydrated DomoPage.

    For a full dashboard (page + cards + layout) use
    ``dashboard_builder.create_dashboard``; this creates the page shell.

    Args:
        auth: DomoAuth instance.
        title: Page title.
        parent_page_id: Parent page id (0 for a top-level page).
        return_raw: Return the raw create response instead of a DomoPage.
        context: Optional RouteContext.
    """
    # create + hydrate reuse one pooled session via auth-level connection
    # reuse (RouteContext.reuse_session, default on).
    res = await page_routes.create_page(
        auth=auth,
        title=title,
        parent_page_id=parent_page_id,
        return_raw=return_raw,
        context=context,
    )

    if return_raw:
        return res

    page_id = res.response.get("pageId") if isinstance(res.response, dict) else None
    if not page_id:
        raise ValueError(
            f"create_page response did not contain a pageId: {res.response!r}"
        )

    return await cls.get_by_id(page_id=str(page_id), auth=auth, context=context)

delete async

delete(
    *, context: RouteContext | None = None, **context_kwargs
)

Delete this page.

Source code in src/crew_dcs/classes/DomoPage/core.py
690
691
692
693
694
695
696
697
698
699
700
async def delete(
    self,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Delete this page."""
    context = RouteContext.build_context(context=context, **context_kwargs)
    return await page_routes.delete_page(
        auth=self.auth, page_id=str(self.id), context=context
    )

get_by_id async classmethod

get_by_id(
    page_id: str,
    auth: DomoAuth,
    suppress_no_results_error: bool = True,
    return_raw: bool = False,
    debug_api: bool = False,
    include_layout: bool = False,
    debug_num_stacks_to_drop=2,
    session: AsyncClient = None,
    id=None,
    check_if_published: bool | None = None,
    parent_auth_retrieval_fn: Callable | None = None,
    parent_auth: DomoAuth | None = None,
    max_subscriptions_to_check: int | None = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Retrieve page data as a DomoPage instance.

Parameters:

Name Type Description Default
page_id str

Page identifier

required
auth DomoAuth

Authentication object

required
suppress_no_results_error bool

Suppress errors when no results found

True
return_raw bool

Return raw API response

False
debug_api bool

Enable debug logging

False
include_layout bool

Include page layout in response

False
debug_num_stacks_to_drop

Stack frames to drop in debug output

2
session AsyncClient

Optional HTTP session

None
id

Alternative parameter for page_id

None
check_if_published bool | None

When True, checks if page is published (requires subscription check). If None and either parent_auth or parent_auth_retrieval_fn is provided, defaults to True.

None
parent_auth_retrieval_fn Callable | None

Callable returning publisher auth when given publisher domain

None
parent_auth DomoAuth | None

Pre-existing publisher auth (alternative to parent_auth_retrieval_fn)

None
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

DomoPage instance

Source code in src/crew_dcs/classes/DomoPage/core.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
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
592
593
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
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
@classmethod
async def get_by_id(
    cls,
    page_id: str,
    auth: DomoAuth,
    suppress_no_results_error: bool = True,
    return_raw: bool = False,
    debug_api: bool = False,
    include_layout: bool = False,
    debug_num_stacks_to_drop=2,
    session: httpx.AsyncClient = None,
    id=None,
    check_if_published: bool | None = None,
    parent_auth_retrieval_fn: Callable | None = None,
    parent_auth: DomoAuth | None = None,
    max_subscriptions_to_check: int | None = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Retrieve page data as a DomoPage instance.

    Args:
        page_id: Page identifier
        auth: Authentication object
        suppress_no_results_error: Suppress errors when no results found
        return_raw: Return raw API response
        debug_api: Enable debug logging
        include_layout: Include page layout in response
        debug_num_stacks_to_drop: Stack frames to drop in debug output
        session: Optional HTTP session
        id: Alternative parameter for page_id
        check_if_published: When True, checks if page is published (requires subscription check).
            If None and either parent_auth or parent_auth_retrieval_fn is provided, defaults to True.
        parent_auth_retrieval_fn: Callable returning publisher auth when given publisher domain
        parent_auth: Pre-existing publisher auth (alternative to parent_auth_retrieval_fn)
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        DomoPage instance
    """
    # Auto-enable publish checking when parent auth is provided (either directly or via retrieval fn)
    # unless explicitly disabled
    if (parent_auth_retrieval_fn or parent_auth) and check_if_published is None:
        check_if_published = True

    page_id = page_id or id

    context = RouteContext.build_context(
        context=context,
        session=session,
        debug_api=debug_api,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        parent_class=cls.__name__,
        **context_kwargs,
    )

    # Get page data
    res = await page_routes.get_page_by_id(
        auth=auth,
        page_id=page_id,
        include_layout=include_layout,
        context=context,
    )

    if return_raw:
        return res

    is_published = False
    subscription = None

    if check_if_published and (parent_auth_retrieval_fn or parent_auth):
        # Create a retrieval function from parent_auth if only parent_auth was provided
        effective_retrieval_fn = parent_auth_retrieval_fn
        if not effective_retrieval_fn and parent_auth:
            effective_retrieval_fn = lambda _domain: parent_auth  # noqa: E731

        # Local import to avoid circular dependency
        from ..subentity.lineage.federation_context import FederationContext

        probe = FederationContext.from_entity_id(
            auth=auth,
            entity_id=str(page_id),
            entity_type="PAGE",
        )
        is_published = await probe.check_if_published(
            retrieve_parent_auth_fn=effective_retrieval_fn,
            entity_type="PAGE",
            session=session,
            debug_api=debug_api,
            max_subscriptions_to_check=max_subscriptions_to_check,
        )
        if is_published:
            subscription = probe.subscription

    # Fetch owners if present in response
    owners = None
    dd = (
        util_dd.DictDot(res.response)
        if isinstance(res.response, dict)
        else res.response
    )
    if hasattr(dd, "page") and dd.page.owners and len(dd.page.owners) > 0:
        pg_temp = DomoPage_Default(
            id=int(dd.id),
            title=dd.title,
            raw=res.response,
            parent_page_id=(
                int(dd.page.parentPageId) if dd.page.parentPageId else None
            ),
            collections=dd.collections,
            auth=auth,
        )
        owners = await pg_temp._get_domo_owners_from_dd(
            dd.page.owners,
            suppress_no_results_error=suppress_no_results_error,
            debug_api=debug_api,
            session=session,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        )

    # Create page instance
    pg = cls._from_content_stacks_v3(
        page_obj=res.response,
        auth=auth,
        owners=owners,
    )

    if is_published and subscription:
        helper = pg.enable_federation_support()
        helper.hydrate_from_existing(
            subscription=subscription,
            parent_auth_retrieval_fn=parent_auth_retrieval_fn,
            parent_auth=parent_auth,
            content_type="PAGE",
            entity_id=str(page_id),
        )

    # Auto-trace lineage if parent_auth_retrieval_fn is provided
    if parent_auth_retrieval_fn and pg.Lineage:
        await pg.Lineage.get(
            parent_auth_retrieval_fn=parent_auth_retrieval_fn,
            parent_auth=parent_auth,
            session=session,
            debug_api=debug_api,
            context=context,
        )

    pg.custom_attributes["parent_page"] = None
    pg.custom_attributes["top_page"] = None

    return pg

DomoPageAccessController

DomoPageAccessController(auth: DomoAuth, parent)

Bases: DomoAccessRelationshipController

Access controller for Domo pages.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
24
25
26
27
def __init__(self, auth: DomoAuth, parent):
    super().__init__(auth=auth, parent_object=parent)
    self.parent = parent
    self.relationships: list[Relationship] = []

add_owners async

add_owners(
    domo_users: list[Any] | None = None,
    domo_groups: list[Any] | None = None,
    user_ids: list[str | int] | None = None,
    group_ids: list[str | int] | None = None,
    message: str | None = None,
    send_email: bool = False,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> bool

Add owners to the page.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
async def add_owners(
    self,
    domo_users: list[Any] | None = None,
    domo_groups: list[Any] | None = None,
    user_ids: list[str | int] | None = None,
    group_ids: list[str | int] | None = None,
    message: str | None = None,
    send_email: bool = False,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> bool:
    """Add owners to the page."""
    context = RouteContext.build_context(context=context, **context_kwargs)

    user_ids = self._collect_ids(domo_users, user_ids)
    group_ids = self._collect_ids(domo_groups, group_ids)

    if not user_ids and not group_ids:
        raise ValueError("Must provide user or group identifiers to add owners")

    res = await page_routes.add_page_owner(
        auth=self.auth,
        page_id_ls=[self.parent_id],
        group_id_ls=group_ids,
        user_id_ls=user_ids,
        note=message or "",
        send_email=send_email,
        return_raw=return_raw,
        context=context,
    )

    return res if return_raw else res.is_success

create_relationship async

create_relationship(
    target_entity_id: str,
    target_entity_type: EntityType,
    relationship_type: RelationshipType,
    **kwargs
) -> bool

Create an access relationship for the page.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
async def create_relationship(
    self,
    target_entity_id: str,
    target_entity_type: EntityType,
    relationship_type: RelationshipType,
    **kwargs,
) -> bool:
    """Create an access relationship for the page."""
    if relationship_type == RelationshipType.HAS_ACCESS_OWNER:
        return await self.add_owners(
            user_ids=(
                [target_entity_id]
                if target_entity_type == EntityType.USER
                else None
            ),
            group_ids=(
                [target_entity_id]
                if target_entity_type == EntityType.GROUP
                else None
            ),
            **kwargs,
        )

    return await self.share(
        user_ids=(
            [target_entity_id] if target_entity_type == EntityType.USER else None
        ),
        group_ids=(
            [target_entity_id] if target_entity_type == EntityType.GROUP else None
        ),
        **kwargs,
    )

get_accesslist async

get_accesslist(
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> list[Relationship]

Get access list for the page as relationships.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
async def get_accesslist(
    self,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> list[Relationship]:
    """Get access list for the page as relationships."""
    context = RouteContext.build_context(context=context, **context_kwargs)

    res = await page_routes.get_page_access_list(
        auth=self.auth,
        page_id=self.parent_id,
        is_expand_users=True,
        return_raw=return_raw,
        context=context,
    )

    if return_raw:
        return res

    relationships: list[Relationship] = []
    users = res.response.get("users", []) if isinstance(res.response, dict) else []
    groups = (
        res.response.get("groups", []) if isinstance(res.response, dict) else []
    )

    for user in users:
        user_id = user.get("id") or user.get("userId")
        if not user_id:
            continue
        relationships.append(
            Relationship(
                from_entity_id=str(user_id),
                from_entity_type=EntityType.USER,
                to_entity_id=str(self.parent_id),
                to_entity_type=EntityType.PAGE,
                relationship_type=self._map_access_level(
                    user.get("accessLevel")
                    or user.get("permission")
                    or user.get("access")
                ),
                metadata={
                    "is_explicit_share": user.get("isExplicitShare", False),
                    **user,
                },
            )
        )

    for group in groups:
        group_id = group.get("id") or group.get("groupId")
        if not group_id:
            continue
        relationships.append(
            Relationship(
                from_entity_id=str(group_id),
                from_entity_type=EntityType.GROUP,
                to_entity_id=str(self.parent_id),
                to_entity_type=EntityType.PAGE,
                relationship_type=self._map_access_level(
                    group.get("accessLevel")
                    or group.get("permission")
                    or group.get("access")
                ),
                metadata=group,
            )
        )

    owner_res = await page_routes.get_page_access_test(
        auth=self.auth,
        page_id=self.parent_id,
        context=context,
    )
    owners = (
        owner_res.response.get("owners", [])
        if isinstance(owner_res.response, dict)
        else []
    )

    for owner in owners:
        owner_id = owner.get("id")
        owner_type = owner.get("type", "USER")
        if not owner_id:
            continue
        existing = next(
            (
                r
                for r in relationships
                if r.from_entity_id == str(owner_id)
                and r.from_entity_type
                == (EntityType.GROUP if owner_type == "GROUP" else EntityType.USER)
            ),
            None,
        )
        if existing:
            existing.relationship_type = RelationshipType.HAS_ACCESS_OWNER
            existing.metadata["is_owner"] = True
        else:
            relationships.append(
                Relationship(
                    from_entity_id=str(owner_id),
                    from_entity_type=(
                        EntityType.GROUP
                        if owner_type == "GROUP"
                        else EntityType.USER
                    ),
                    to_entity_id=str(self.parent_id),
                    to_entity_type=EntityType.PAGE,
                    relationship_type=RelationshipType.HAS_ACCESS_OWNER,
                    metadata={"is_owner": True, **owner},
                )
            )

    self.relationships = relationships
    self.invalidate_cache()
    return relationships

get_direct_relationships async

get_direct_relationships() -> list[Relationship]

Get direct relationships for the page.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
33
34
35
async def get_direct_relationships(self) -> list[Relationship]:
    """Get direct relationships for the page."""
    return await self.get_accesslist()

get_owners async

get_owners(
    *, context: RouteContext | None = None, **context_kwargs
) -> list[Relationship]

Get owner relationships for the page.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
async def get_owners(
    self,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> list[Relationship]:
    """Get owner relationships for the page."""
    if not self.relationships:
        await self.get_accesslist(context=context, **context_kwargs)

    return [
        rel
        for rel in self.relationships
        if rel.relationship_type == RelationshipType.HAS_ACCESS_OWNER
    ]

remove_owners async

remove_owners(
    domo_users: list[Any] | None = None,
    domo_groups: list[Any] | None = None,
    user_ids: list[str | int] | None = None,
    group_ids: list[str | int] | None = None,
    **_kwargs
) -> bool

Remove owners from the page.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
265
266
267
268
269
270
271
272
273
274
275
276
async def remove_owners(
    self,
    domo_users: list[Any] | None = None,
    domo_groups: list[Any] | None = None,
    user_ids: list[str | int] | None = None,
    group_ids: list[str | int] | None = None,
    **_kwargs,
) -> bool:
    """Remove owners from the page."""
    raise NotImplementedError(
        "Removing page owners is not supported by current page routes."
    )

remove_relationship async

remove_relationship(
    target_entity_id: str,
    target_entity_type: EntityType,
    relationship_type: RelationshipType,
    **kwargs
) -> bool

Remove an access relationship for the page.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
async def remove_relationship(
    self,
    target_entity_id: str,
    target_entity_type: EntityType,
    relationship_type: RelationshipType,
    **kwargs,
) -> bool:
    """Remove an access relationship for the page."""
    if relationship_type == RelationshipType.HAS_ACCESS_OWNER:
        return await self.remove_owners(
            user_ids=(
                [target_entity_id]
                if target_entity_type == EntityType.USER
                else None
            ),
            group_ids=(
                [target_entity_id]
                if target_entity_type == EntityType.GROUP
                else None
            ),
            **kwargs,
        )

    raise NotImplementedError(
        "Removing page share relationships is not supported by current routes."
    )

share async

share(
    domo_users: list[Any] | None = None,
    domo_groups: list[Any] | None = None,
    user_ids: list[str | int] | None = None,
    group_ids: list[str | int] | None = None,
    message: str | None = None,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> bool

Share the page with users or groups.

Source code in src/crew_dcs/classes/DomoPage/access_controller.py
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
async def share(
    self,
    domo_users: list[Any] | None = None,
    domo_groups: list[Any] | None = None,
    user_ids: list[str | int] | None = None,
    group_ids: list[str | int] | None = None,
    message: str | None = None,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> bool:
    """Share the page with users or groups."""
    context = RouteContext.build_context(context=context, **context_kwargs)

    user_ids = self._collect_ids(domo_users, user_ids)
    group_ids = self._collect_ids(domo_groups, group_ids)

    if not user_ids and not group_ids:
        raise ValueError("Must provide user or group identifiers to share page")

    res = await share_resource(
        auth=self.auth,
        resource_ids=self.parent_id,
        resource_type=ShareResource_Enum.PAGE,
        group_ids=group_ids,
        user_ids=user_ids,
        message=message,
        return_raw=return_raw,
        context=context,
    )

    return res if return_raw else res.is_success

DomoPageLayout dataclass

DomoPageLayout(
    parent: DomoEntity,
    layout: PageLayout | None = None,
    cards: list[Any] = list(),
    datasets: list[Any] = list(),
)

Bases: DomoSubEntity

Page layout and content management subentity.

Manages page layout configuration and content operations including cards and datasets. Combines layout management with content operations that require page definition.

Attributes:

Name Type Description
layout PageLayout | None

PageLayout configuration object

cards list[Any]

list of DomoCard objects on the page

datasets list[Any]

list of DomoDataset objects used by page cards

Example

page = await DomoPage.get_by_id(page_id="123", auth=auth)

Get cards from page

await page.Layout.get_cards() print(f"Found {len(page.Layout.cards)} cards")

Update layout

await page.Layout.update(layout_changes={...})

get async

get(
    *, context: RouteContext | None = None, **context_kwargs
) -> PageLayout

Fetch the page layout from the v4/layouts API.

Retrieves the full layout including content slots and grid templates. Stores the result in self.layout for subsequent operations.

Parameters:

Name Type Description Default
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description
PageLayout

PageLayout object with content slots and grid templates

Source code in src/crew_dcs/classes/DomoPage/layout.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
async def get(
    self,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> PageLayout:
    """Fetch the page layout from the v4/layouts API.

    Retrieves the full layout including content slots and grid templates.
    Stores the result in self.layout for subsequent operations.

    Args:
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        PageLayout object with content slots and grid templates
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    # First, get the layout ID from the page definition
    if not self.layout:
        res = await page_routes.get_page_by_id(
            auth=self.parent.auth,
            page_id=self.parent.id,
            include_layout=True,
            context=context,
        )
        page_data = res.response
        if isinstance(page_data, dict):
            page_data = util_dd.DictDot(page_data)
        if hasattr(page_data, "pageLayoutV4") and page_data.pageLayoutV4:
            self.layout = PageLayout.from_dict(dd=page_data.pageLayoutV4)

    if not self.layout:
        # Fallback: try the direct layout endpoint if we know the layout_id
        raise ValueError(
            f"Could not determine layout ID for page {self.parent.id}. "
            "Fetch the page definition first with include_layout=True."
        )

    # Now fetch the full layout from the v4 endpoint (more complete than pageLayoutV4)
    res = await page_routes.get_page_layout(
        auth=self.parent.auth,
        layout_id=str(self.layout.id),
        context=context,
    )
    dd = (
        util_dd.DictDot(res.response)
        if isinstance(res.response, dict)
        else res.response
    )
    self.layout = PageLayout.from_dict(dd=dd)
    return self.layout

get_cards async

get_cards(
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    parent_auth_retrieval_fn: (
        Callable[[str], Any] | None
    ) = None,
    check_if_published: bool | None = None,
    **context_kwargs
)

Get all cards from the page.

Fetches page definition and returns all cards, optionally checking if they're published.

Parameters:

Name Type Description Default
return_raw bool

Return raw ResponseGetData without processing

False
context RouteContext | None

Optional RouteContext for API call configuration

None
parent_auth_retrieval_fn Callable[[str], Any] | None

Callable returning publisher auth when given publisher domain

None
check_if_published bool | None

Check if cards are published (requires subscription check)

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

list of DomoCard objects or ResponseGetData if return_raw=True

Source code in src/crew_dcs/classes/DomoPage/layout.py
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
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
async def get_cards(
    self,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    parent_auth_retrieval_fn: Callable[[str], Any] | None = None,
    check_if_published: bool | None = None,
    **context_kwargs,
):
    """Get all cards from the page.

    Fetches page definition and returns all cards, optionally checking if they're published.

    Args:
        return_raw: Return raw ResponseGetData without processing
        context: Optional RouteContext for API call configuration
        parent_auth_retrieval_fn: Callable returning publisher auth when given publisher domain
        check_if_published: Check if cards are published (requires subscription check)
        **context_kwargs: Additional context parameters

    Returns:
        list of DomoCard objects or ResponseGetData if return_raw=True
    """
    from .. import DomoCard as dc

    context = RouteContext.build_context(context=context, **context_kwargs)

    res = await page_routes.get_page_definition(
        auth=self.parent.auth, page_id=self.parent.id, context=context
    )

    if return_raw:
        return res

    if len(res.response.get("cards")) == 0:
        return []

    check_publish = True if check_if_published is None else check_if_published

    self.cards = await dmce.gather_with_concurrency(
        n=60,
        *[  # noqa: B026
            dc.DomoCard.get_by_id(
                card_id=card["id"],
                auth=self.parent.auth,
                parent_auth_retrieval_fn=parent_auth_retrieval_fn,
                check_if_published=check_publish,
                context=context,
            )
            for card in res.response.get("cards")
        ],
    )

    # Auto-fetch datasets with lineage for each card when parent_auth_retrieval_fn is provided
    if parent_auth_retrieval_fn:
        await dmce.gather_with_concurrency(
            n=60,
            *[  # noqa: B026
                card.Datasets.get(
                    parent_auth_retrieval_fn=parent_auth_retrieval_fn,
                    context=context,
                )
                for card in self.cards
                if card.Datasets
            ],
        )

    return self.cards

get_datasets async

get_datasets(
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Get all datasets used by cards on the page.

Parameters:

Name Type Description Default
return_raw bool

Return raw ResponseGetData without processing

False
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

list of DomoDataset objects or ResponseGetData if return_raw=True

Source code in src/crew_dcs/classes/DomoPage/layout.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
async def get_datasets(
    self,
    return_raw: bool = False,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Get all datasets used by cards on the page.

    Args:
        return_raw: Return raw ResponseGetData without processing
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        list of DomoDataset objects or ResponseGetData if return_raw=True
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    res = await page_routes.get_page_definition(
        auth=self.parent.auth, page_id=self.parent.id, context=context
    )

    if return_raw:
        return res

    cards = await self.get_cards(context=context)

    card_datasets = await dmce.gather_with_concurrency(
        *[card.get_datasets(context=context) for card in cards],
        n=10,
    )

    self.datasets = [
        ds for ds_ls in card_datasets for ds in ds_ls if ds is not None
    ]

    return self.datasets

swap_cards async

swap_cards(
    card_mappings: dict[int, int],
    *,
    context: RouteContext | None = None,
    **context_kwargs
) -> bool

Swap card IDs in the layout content slots.

Replaces cards in the layout by content_key. Acquires write lock, updates layout, then releases lock.

Appendix behavior: The Domo layout API is append-only for content slots. When you change a cardId on a slot, the old card is NOT deleted — it gets displaced to the "appendix" (a hidden section of the page where unplaced content lives). Each swap adds a HEADER + the old card to the appendix. Repeated swaps will accumulate orphaned cards in the appendix. To clean up, use the Domo UI to delete appendix content, or call remove_appendix() after swapping.

Parameters:

Name Type Description Default
card_mappings dict[int, int]

Dict mapping content_key → new card_id. Example: {0: 123456, 2: 789012} replaces the card at content_key 0 with card 123456 and content_key 2 with card 789012.

required
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description
bool

True if successful, False otherwise

Source code in src/crew_dcs/classes/DomoPage/layout.py
550
551
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
592
593
async def swap_cards(
    self,
    card_mappings: dict[int, int],
    *,
    context: RouteContext | None = None,
    **context_kwargs,
) -> bool:
    """Swap card IDs in the layout content slots.

    Replaces cards in the layout by content_key. Acquires write lock,
    updates layout, then releases lock.

    **Appendix behavior**: The Domo layout API is append-only for content
    slots. When you change a cardId on a slot, the old card is NOT deleted —
    it gets displaced to the "appendix" (a hidden section of the page where
    unplaced content lives). Each swap adds a HEADER + the old card to the
    appendix. Repeated swaps will accumulate orphaned cards in the appendix.
    To clean up, use the Domo UI to delete appendix content, or call
    remove_appendix() after swapping.

    Args:
        card_mappings: Dict mapping content_key → new card_id.
            Example: {0: 123456, 2: 789012} replaces the card at
            content_key 0 with card 123456 and content_key 2 with card 789012.
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        True if successful, False otherwise
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    if not self.layout:
        await self.get(context=context)

    # Apply card swaps to content slots
    for content_item in self.layout.content:
        if content_item.content_key in card_mappings:
            new_card_id = card_mappings[content_item.content_key]
            content_item.card_id = new_card_id
            content_item.card_urn = str(new_card_id)

    # Push the updated layout (update() uses self.layout.get_body() internally)
    return await self.update(context=context)

update async

update(
    body: dict | None = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

Update page layout.

Acquires write lock, updates layout, then releases lock. If no body is provided, uses self.layout.get_body().

Parameters:

Name Type Description Default
body dict | None

Layout configuration dictionary (optional — uses self.layout.get_body() if not provided)

None
context RouteContext | None

Optional RouteContext for API call configuration

None
**context_kwargs

Additional context parameters

{}

Returns:

Type Description

True if successful, False otherwise

Source code in src/crew_dcs/classes/DomoPage/layout.py
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
async def update(
    self,
    body: dict | None = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """Update page layout.

    Acquires write lock, updates layout, then releases lock.
    If no body is provided, uses self.layout.get_body().

    Args:
        body: Layout configuration dictionary (optional — uses self.layout.get_body() if not provided)
        context: Optional RouteContext for API call configuration
        **context_kwargs: Additional context parameters

    Returns:
        True if successful, False otherwise
    """
    context = RouteContext.build_context(context=context, **context_kwargs)

    if not self.layout:
        # Need layout ID - fetch it first
        res = await page_routes.get_page_by_id(
            auth=self.parent.auth,
            page_id=self.parent.id,
            include_layout=True,
            context=context,
        )
        page_data = res.response
        if isinstance(page_data, dict):
            page_data = util_dd.DictDot(page_data)
        if hasattr(page_data, "pageLayoutV4") and page_data.pageLayoutV4:
            self.layout = PageLayout.from_dict(dd=page_data.pageLayoutV4)

    if not self.layout:
        return False

    layout_id = self.layout.id
    layout_body = body or self.layout.get_body()

    datetime_now = dt.datetime.now()
    start_time_epoch = dmcv.convert_datetime_to_epoch_millisecond(datetime_now)

    res_writelock = await page_routes.put_writelock(
        auth=self.parent.auth,
        layout_id=layout_id,
        user_id=self.parent.auth.user_id,
        epoch_time=start_time_epoch,
        context=context,
    )

    if res_writelock.status == 200:
        res = await page_routes.update_page_layout(
            auth=self.parent.auth,
            body=layout_body,
            layout_id=layout_id,
            context=context,
        )

        if not res.is_success:
            return False

        res_writelock = await page_routes.delete_writelock(
            auth=self.parent.auth, layout_id=layout_id, context=context
        )
        if res_writelock.status != 200:
            return False

    else:
        return False

    return True

DomoPage_Default dataclass

DomoPage_Default(
    auth: DomoAuth,
    id: int,
    raw: dict,
    Lineage: DomoLineage | None = None,
    title: str | None = None,
    top_page_id: int | None = None,
    parent_page_id: int | None = None,
    is_locked: bool | None = None,
    collections: list[dict] = list(),
    custom_attributes: dict[str, Any] = dict(),
    parent_page: dict[str, Any] | None = None,
    top_page: dict[str, Any] | None = None,
    children: list[Any] = list(),
    Access: DomoPageAccessController | None = None,
    Layout: DomoPageLayout | None = None,
)

Bases: DomoEntity_w_Lineage

entity_name property

entity_name: str

Get the display name for this page.

Pages use the 'title' field as their display name.

Returns:

Type Description
str

Page title, or page ID as fallback

is_federated property

is_federated: bool

Check if this page is federated (published/subscribed).

name property

name: str

Get the display name for this page.

Implements abstract property from DomoEntity_w_Lineage. Pages use the 'title' field as their display name.

Returns:

Type Description
str

Page title, or "Untitled Page {id}" as fallback

to_dict

to_dict(
    override_fn: Callable | None = None,
    return_snake_case: bool = False,
) -> dict

Serialize page for export/debugging with layout card details.

Source code in src/crew_dcs/classes/DomoPage/core.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def to_dict(
    self, override_fn: Callable | None = None, return_snake_case: bool = False
) -> dict:
    """Serialize page for export/debugging with layout card details."""
    if override_fn:
        return override_fn(self)

    result = super().to_dict(return_snake_case=return_snake_case)

    layout_key = "layout" if return_snake_case else "Layout"
    cards_key = "cards" if return_snake_case else "Cards"

    def _serialize_card(card: Any) -> Any:
        return self._serialize_related_entity(
            card,
            return_snake_case=return_snake_case,
            include_entity_type=True,
        )

    if self.Layout and self.Layout.cards:
        layout_payload: dict[str, Any] = {
            cards_key: [
                c
                for c in (_serialize_card(c) for c in self.Layout.cards)
                if c is not None
            ]
        }
        result[layout_key] = layout_payload

    return result

DomoPages dataclass

DomoPages(auth: DomoAuth, pages: list[Any] = None)

get async

get(**kwargs)

calls get_admin_summary to retrieve all pages in an instance

Source code in src/crew_dcs/classes/DomoPage/pages.py
21
22
23
async def get(self, **kwargs):
    """calls get_admin_summary to retrieve all pages in an instance"""
    return await self.get_admin_summary(**kwargs)

get_admin_summary async

get_admin_summary(
    search_title: str | None = None,
    parent_page_id: int | None = None,
    return_raw: bool = False,
    debug_api: bool = False,
    session: AsyncClient = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs
)

use admin_summary to retrieve all pages in an instance -- regardless of user access NOTE: some Page APIs will not return results if page access isn't explicitly shared

Source code in src/crew_dcs/classes/DomoPage/pages.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
async def get_admin_summary(
    self,
    search_title: str | None = None,
    parent_page_id: int | None = None,
    return_raw: bool = False,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    *,
    context: RouteContext | None = None,
    **context_kwargs,
):
    """use admin_summary to retrieve all pages in an instance -- regardless of user access
    NOTE: some Page APIs will not return results if page access isn't explicitly shared
    """
    from .core import DomoPage

    # DomoPages doesn't extend DomoManager, so use RouteContext.build_context directly
    # and merge with context_kwargs
    context = RouteContext.build_context(
        context=context,
        session=session,
        debug_api=debug_api,
        parent_class=self.__class__.__name__,
        **context_kwargs,
    )

    res = await page_routes.get_pages_adminsummary(
        auth=self.auth,
        context=context,
        search_title=search_title,
        page_parent_id=parent_page_id,
    )

    if return_raw:
        return res

    self.pages = await dmce.gather_with_concurrency(
        n=60,
        *[  # noqa: B026
            DomoPage._from_adminsummary(
                page_obj, auth=self.auth, debug_api=debug_api, session=session
            )
            for page_obj in res.response
        ],
    )

    return self.pages

Modules