Esri / arcgis-python-api

Documentation and samples for ArcGIS API for Python
https://developers.arcgis.com/python/
Apache License 2.0
1.89k stars 1.1k forks source link

Swap the source of a hosted layer view using the ArcGIS API for Python #1731

Closed GeeFernando closed 10 months ago

GeeFernando commented 10 months ago

Swap the source of a hosted layer view using the ArcGIS API for Python Do you know whether it is possible to swap the source of a hosted layer view using the ArcGIS API for Python? This can be done using AGOL - https://www.esri.com/arcgis-blog/products/arcgis-online/data-management/swapping-layers-a-great-way-to-build-and-maintain-your-feature-layer/. But I was wondering if the same could be done using the ArcGIS API for Python.

Additional context Currently, I have a Python script that appends around 250,000 features to an ArcGIS Online feature layer. This script runs every day, but the append process can take anywhere from a couple of seconds to 10 minutes. However, during the appending process, the dashboard becomes unusable. If I could perform a layer swap, it would solve my issue 🤗 I'm just wondering whether you could achieve this using the ArcGIS API for Python 🤔

GeeFernando commented 10 months ago

ReadMe.pdf I just found this article (page 24) which mentions some methods around swapping layers. But these methods aren't documented in the API reference - https://developers.arcgis.com/python/api-reference/

achapkowski commented 10 months ago

You need to do a delete_from_definition, then add_to_definition to add the new source dataset.

Sample Code (probably doesn't cover all cases)

from __future__ import annotations
from arcgis.gis import GIS, Item
from arcgis.features import FeatureLayer, FeatureLayerCollection, Table
import concurrent.futures

def swap_view(
    view: FeatureLayerCollection,
    index: int,
    new_source: FeatureLayer | Table,
    future: bool = False,
) -> dict | concurrent.futures.Future:
    """
    Swaps the Data Source Layer with a different parent layer.

    ==================     ====================================================================
    **Parameter**           **Description**
    ------------------     --------------------------------------------------------------------
    view                   Required FeatureLayerCollection. The view feature layer collection
                           to update.
    ------------------     --------------------------------------------------------------------
    index                  Required int. The index of the layer on the view to replace.
    ------------------     --------------------------------------------------------------------
    new_source             Requred FeatureLayer or Table. The layer to replace the existing
                           source with.
    ------------------     --------------------------------------------------------------------
    future                 Optional Bool. When True, a Future object will be returned else a
                           JSON object.
    ==================     ====================================================================

    """
    keys: list[str] = [
        'currentVersion',
        'id',
        'name',
        'type',
        'displayField',
        'description',
        'copyrightText',
        'defaultVisibility',
        'editingInfo',
        'isDataVersioned',
        'hasContingentValuesDefinition',
        'supportsAppend',
        'supportsCalculate',
        'supportsASyncCalculate',
        'supportsTruncate',
        'supportsAttachmentsByUploadId',
        'supportsAttachmentsResizing',
        'supportsRollbackOnFailureParameter',
        'supportsStatistics',
        'supportsExceedsLimitStatistics',
        'supportsAdvancedQueries',
        'supportsValidateSql',
        'supportsCoordinatesQuantization',
        'supportsLayerOverrides',
        'supportsTilesAndBasicQueriesMode',
        'supportsFieldDescriptionProperty',
        'supportsQuantizationEditMode',
        'supportsApplyEditsWithGlobalIds',
        'supportsMultiScaleGeometry',
        'supportsReturningQueryGeometry',
        'hasGeometryProperties',
        'geometryProperties',
        'advancedQueryCapabilities',
        'advancedQueryAnalyticCapabilities',
        'advancedEditingCapabilities',
        'infoInEstimates',
        'useStandardizedQueries',
        'geometryType',
        'minScale',
        'maxScale',
        'extent',
        'drawingInfo',
        'allowGeometryUpdates',
        'hasAttachments',
        'htmlPopupType',
        'hasMetadata',
        'hasM',
        'hasZ',
        'objectIdField',
        'uniqueIdField',
        'globalIdField',
        'typeIdField',
        'dateFieldsTimeReference',
        'preferredTimeReference',
        'types',
        'templates',
        'supportedQueryFormats',
        'supportedAppendFormats',
        'supportedExportFormats',
        'supportedSpatialRelationships',
        'supportedContingentValuesFormats',
        'supportedSyncDataOptions',
        'hasStaticData',
        'maxRecordCount',
        'standardMaxRecordCount',
        'standardMaxRecordCountNoGeometry',
        'tileMaxRecordCount',
        'maxRecordCountFactor',
        'capabilities',
        'url',
        'adminLayerInfo',
    ]
    if isinstance(new_source, FeatureLayer):
        flc_lyr_info: FeatureLayer = view.layers[index]
    elif isinstance(new_source, Table):
        flc_lyr_info: Table = view.tables[index]
    props: dict = {
        key: new_source.properties[key]
        for key in keys
        if key in new_source.properties
    }
    if new_source._con.token:
        props['url'] = new_source.url + f"?token={new_source._con.token}"
    else:
        props['url'] = new_source.url
    if (
        "viewLayerDefinition"
        in flc_lyr_info.manager.properties['adminLayerInfo']
    ):
        props['adminLayerInfo'] = {}
        props['adminLayerInfo'][
            'viewLayerDefinition'
        ] = flc_lyr_info.manager.properties['adminLayerInfo'][
            'viewLayerDefinition'
        ]
        props['adminLayerInfo']['viewLayerDefinition'][
            'sourceServiceName'
        ] = new_source.manager.properties['name']
        props['adminLayerInfo']['viewLayerDefinition'].pop("sourceId", None)
    if isinstance(new_source, FeatureLayer):
        delete_json: dict = {"layers": [{"id": index}], "tables": []}
        add_json: dict = {"layers": [props]}
    elif isinstance(new_source, Table):
        delete_json: dict = {"layers": [], "tables": [{"id": index}]}
        add_json: dict = {"tables": [props]}
    view.manager.delete_from_definition(delete_json)
    if future:
        return view.manager.add_to_definition(add_json, future=True)
    else:
        return view.manager.add_to_definition(add_json, future=False)

Usage

   gis = GIS(
        profile='your_online_profile'
    )
    #  Get the Item View
    #
    source_item: Item = gis.content.get("ITEM ID")
    #  Obtain the FeatureLayerCollection from the Item (from the view Item)
    #
    source_view: FeatureLayerCollection = FeatureLayerCollection.fromitem(
        source_item
    )
    #  Get the Replace Item
    #
    replace_item: Item = gis.content.get("ITEM ID")
    #  Get the FeatureLayer/Table to update
    #
    replace_layer: FeatureLayer = FeatureLayer.fromitem(replace_item)

    #  Swap the View to the New Source (replace_layer object)
    #
    swap_view(view=source_view, new_source=replace_layer, index=0)
GeeFernando commented 10 months ago

Thanks so much for this @achapkowski 🤩

Robert-Holliday commented 1 week ago

Has anyone figured out how to make this work when the new source FLC and the target view have multiple layers?