Example #1
0
def update_by_value(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    value: Any,
    dataset_where_sql: Optional[str] = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by assigning a given value.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        value: Value to assign.
        dataset_where_sql: SQL where-clause for dataset subselection.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by given value.",
        dataset_path,
        field_name,
    )
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=[field_name],
        where_clause=dataset_where_sql,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for (old_value, ) in cursor:
            if same_value(old_value, value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow([value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Example #2
0
def update_rows(
    dataset_path: Union[Path, str],
    *,
    field_name: str,
    id_field_names: Iterable[str],
    cmp_dataset_path: Union[Path, str],
    cmp_field_name: Optional[str] = None,
    cmp_id_field_names: Optional[Iterable[str]] = None,
    cmp_date: Optional[Union[date, _datetime]] = None,
    date_initiated_field_name: str = "date_initiated",
    date_expired_field_name: str = "date_expired",
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Add field value changes to tracking dataset from comparison dataset.

    Args:
        dataset_path: Path to tracking dataset.
        field_name: Name of field with tracked attribute.
        id_field_names: Names of the feature ID fields.
        cmp_dataset_path: Path to comparison dataset.
        cmp_field_name: Name of field with tracked attribute in comparison dataset. If
            set to None, will assume same as field_name.
        cmp_id_field_names: Names of the feature ID fields in comparison dataset. If set
            to None, will assume same as field_name.
        cmp_date: Date to mark comparison change. If set to None, will set to the date
            of execution.
        date_initiated_field_name: Name of tracking-row-inititated date field.
        date_expired_field_name: Name of tracking-row-expired date field.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Feature counts for each update-state.
    """
    dataset_path = Path(dataset_path)
    cmp_dataset_path = Path(cmp_dataset_path)
    LOG.log(
        log_level,
        "Start: Update tracking rows in `%s` from `%s`.",
        dataset_path,
        cmp_dataset_path,
    )
    id_field_names = list(id_field_names)
    if cmp_field_name is None:
        cmp_field_name = field_name
    cmp_id_field_names = (id_field_names if cmp_id_field_names is None else
                          list(cmp_id_field_names))
    if cmp_date is None:
        cmp_date = date.today()
    current_where_sql = f"{date_expired_field_name} IS NULL"
    id_current_value = {
        row[:-1]: row[-1]
        for row in features.as_tuples(
            dataset_path,
            field_names=id_field_names + [field_name],
            dataset_where_sql=current_where_sql,
        )
    }
    id_cmp_value = {
        row[:-1]: row[-1]
        for row in features.as_tuples(cmp_dataset_path,
                                      field_names=cmp_id_field_names +
                                      [cmp_field_name])
    }
    changed_ids = set()
    expired_ids = {_id for _id in id_current_value if _id not in id_cmp_value}
    new_rows = []
    for _id, value in id_cmp_value.items():
        if _id not in id_current_value:
            new_rows.append(_id + (value, cmp_date))
        elif not same_value(value, id_current_value[_id]):
            changed_ids.add(_id)
            new_rows.append(_id + (value, cmp_date))
    # ArcPy2.8.0: Convert Path to str.
    cursor = arcpy.da.UpdateCursor(
        in_table=str(dataset_path),
        field_names=id_field_names + [field_name, date_expired_field_name],
        where_clause=current_where_sql,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for row in cursor:
            _id = tuple(row[:len(id_field_names)])
            if _id in changed_ids or _id in expired_ids:
                cursor.updateRow(_id + (row[-2], cmp_date))
            else:
                states["unchanged"] += 1
    features.insert_from_iters(
        dataset_path,
        field_names=id_field_names + [field_name, date_initiated_field_name],
        source_features=new_rows,
        use_edit_session=use_edit_session,
        log_level=logging.DEBUG,
    )
    states["changed"] = len(changed_ids)
    states["expired"] = len(expired_ids)
    log_entity_states("features", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Example #3
0
def consolidate_rows(
    dataset_path: Union[Path, str],
    *,
    field_name: str,
    id_field_names: Iterable[str],
    date_initiated_field_name: str = "date_initiated",
    date_expired_field_name: str = "date_expired",
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Consolidate tracking dataset rows where the value does not actually change.

    Useful for quick-loaded point-in-time values, or for processing hand-altered rows.

    Args:
        dataset_path: Path to tracking dataset.
        field_name: Name of field with tracked attribute.
        id_field_names: Names of the feature ID fields.
        date_initiated_field_name: Name of tracking-row-inititated date field.
        date_expired_field_name: Name of tracking-row-expired date field.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Feature counts for each update-state.
    """
    dataset_path = Path(dataset_path)
    LOG.log(log_level, "Start: Consolidate tracking rows in `%s`.",
            dataset_path)
    id_field_names = list(id_field_names)
    field_names = id_field_names + [
        date_initiated_field_name,
        date_expired_field_name,
        field_name,
    ]
    id_rows = defaultdict(list)
    for row in features.as_dicts(dataset_path, field_names=field_names):
        _id = tuple(row[name] for name in id_field_names)
        id_rows[_id].append(row)
    for _id in list(id_rows):
        rows = sorted(id_rows[_id], key=itemgetter(date_initiated_field_name))
        for i, row in enumerate(rows):
            if i == 0 or row[date_initiated_field_name] is None:
                continue

            date_initiated = row[date_initiated_field_name]
            value = row[field_name]
            previous_row = rows[i - 1]
            previous_value = previous_row[field_name]
            previous_date_expired = previous_row[date_expired_field_name]
            if same_value(value, previous_value) and same_value(
                    date_initiated, previous_date_expired):
                # Move previous row date initiated to current row & clear from previous.
                row[date_initiated_field_name] = previous_row[
                    date_initiated_field_name]
                previous_row[date_initiated_field_name] = None
        id_rows[_id] = [
            row for row in rows if row[date_initiated_field_name] is not None
        ]
    states = features.update_from_dicts(
        dataset_path,
        field_names=field_names,
        # In tracking dataset, ID is ID + date_initiated.
        id_field_names=id_field_names + [date_initiated_field_name],
        source_features=chain(*id_rows.values()),
        use_edit_session=use_edit_session,
        log_level=logging.DEBUG,
    )
    log_entity_states("tracking rows", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Consolidate.")
    return states
Example #4
0
def update_by_overlay_count(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    overlay_dataset_path: Union[Path, str],
    dataset_where_sql: Optional[str] = None,
    overlay_where_sql: Optional[str] = None,
    tolerance: Optional[float] = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by count of overlay features.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        overlay_dataset_path: Path to overlay-dataset.

    Keyword Args:
        dataset_where_sql: SQL where-clause for dataset subselection.
        overlay_where_sql: SQL where-clause for overlay-dataset subselection.
        tolerance: Tolerance for coincidence, in units of the dataset. If set to None,
            will use the default tolerance for the workspace of the dataset.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    overlay_dataset_path = Path(overlay_dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by overlay feature counts from `%s`.",
        dataset_path,
        field_name,
        overlay_dataset_path,
    )
    original_tolerance = arcpy.env.XYTolerance
    view = DatasetView(dataset_path,
                       field_names=[],
                       dataset_where_sql=dataset_where_sql)
    overlay_view = DatasetView(
        overlay_dataset_path,
        field_names=[],
        dataset_where_sql=overlay_where_sql,
    )
    with view, overlay_view:
        if tolerance is not None:
            arcpy.env.XYTolerance = tolerance
        temp_output_path = unique_path("output")
        arcpy.analysis.SpatialJoin(
            target_features=view.name,
            join_features=overlay_view.name,
            # ArcPy2.8.0: Convert to str.
            out_feature_class=str(temp_output_path),
            join_operation="JOIN_ONE_TO_ONE",
            join_type="KEEP_COMMON",
            match_option="INTERSECT",
        )
    arcpy.env.XYTolerance = original_tolerance
    cursor = arcpy.da.SearchCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(temp_output_path),
        field_names=["TARGET_FID", "Join_Count"],
    )
    with cursor:
        oid_overlay_count = dict(cursor)
    # ArcPy2.8.0: Convert to str.
    arcpy.management.Delete(str(temp_output_path))
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=["OID@", field_name],
        where_clause=dataset_where_sql,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for feature in cursor:
            oid = feature[0]
            old_value = feature[1]
            new_value = oid_overlay_count.get(oid, 0)
            if same_value(old_value, new_value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow([oid, new_value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{new_value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Example #5
0
def update_by_mapping(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    mapping: Union[Mapping, FunctionType],
    key_field_names: Iterable[str],
    dataset_where_sql: Optional[str] = None,
    default_value: Any = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by finding them in a mapping.

    Notes:
        Mapping key must be a tuple if an iterable.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        mapping: Mapping to get values from.
        key_field_names: Names of mapping key fields.
        dataset_where_sql: SQL where-clause for dataset subselection.
        default_value: Value to assign mapping if key value not in mapping.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    key_field_names = list(key_field_names)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by mapping with key in `%s`.",
        dataset_path,
        field_name,
        key_field_names,
    )
    if isinstance(mapping, EXECUTABLE_TYPES):
        mapping = mapping()
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=key_field_names + [field_name],
        where_clause=dataset_where_sql,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for feature in cursor:
            key = feature[0] if len(key_field_names) == 1 else tuple(
                feature[:-1])
            old_value = feature[-1]
            new_value = mapping.get(key, default_value)
            if same_value(old_value, new_value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow(feature[:-1] + [new_value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{new_value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Example #6
0
def update_by_joined_value(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    key_field_names: Iterable[str],
    join_dataset_path: Union[Path, str],
    join_field_name: str,
    join_key_field_names: Iterable[str],
    dataset_where_sql: Optional[str] = None,
    join_dataset_where_sql: Optional[str] = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by referencing a joinable field in another dataset.

    key_field_names & join_key_field_names must be the same length & same order.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        key_field_names: Names of relationship key fields.
        join_dataset_path: Path to join-dataset.
        join_field_name: Name of join-field.
        join_key_field_names: Names of relationship key fields on join-dataset.
        dataset_where_sql: SQL where-clause for dataset subselection.
        join_dataset_where_sql: SQL where-clause for join-dataset subselection.
        use_edit_session: Updates are done in an edit session if True.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        AttributeError: If key_field_names & join_key_field_names have different length.
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    join_dataset_path = Path(join_dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by joined values in `%s.%s`.",
        dataset_path,
        field_name,
        join_dataset_path,
        join_field_name,
    )
    key_field_names = list(key_field_names)
    join_key_field_names = list(join_key_field_names)
    if len(key_field_names) != len(join_key_field_names):
        raise AttributeError(
            "key_field_names & join_key_field_names not same length.")

    cursor = arcpy.da.SearchCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(join_dataset_path),
        field_names=join_key_field_names + [join_field_name],
        where_clause=join_dataset_where_sql,
    )
    with cursor:
        id_join_value = {feature[:-1]: feature[-1] for feature in cursor}
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=key_field_names + [field_name],
        where_clause=dataset_where_sql,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for feature in cursor:
            old_value = feature[-1]
            new_value = id_join_value.get(tuple(feature[:-1]))
            if same_value(old_value, new_value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow(feature[:-1] + [new_value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{new_value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Example #7
0
def update_by_function(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    function: FunctionType,
    field_as_first_arg: bool = True,
    arg_field_names: Iterable[str] = (),
    kwarg_field_names: Iterable[str] = (),
    dataset_where_sql: Optional[str] = None,
    spatial_reference_item: SpatialReferenceSourceItem = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by passing them to a function.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        function: Function to return values from.
        field_as_first_arg: True if field value will be the first positional argument.
        arg_field_names: Field names whose values will be the function positional
            arguments (not including primary field).
        kwarg_field_names: Field names whose names & values will be the function keyword
            arguments.
        dataset_where_sql: SQL where-clause for dataset subselection.
        spatial_reference_item: Item from which the spatial reference for any geometry
            properties will be set to. If set to None, will use spatial reference of
            the dataset.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by function `%s`.",
        dataset_path,
        field_name,
        # Partials show all the pre-loaded arg & kwarg values, which is cumbersome.
        "partial version of function {}".format(function.func) if isinstance(
            function, partial) else "function `{}`".format(function),
    )
    arg_field_names = list(arg_field_names)
    kwarg_field_names = list(kwarg_field_names)
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=arg_field_names + kwarg_field_names + [field_name],
        where_clause=dataset_where_sql,
        spatial_reference=SpatialReference(spatial_reference_item).object,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for feature in cursor:
            old_value = feature[-1]
            args = feature[:len(arg_field_names)]
            if field_as_first_arg:
                args = [old_value] + args
            kwargs = dict(
                zip(kwarg_field_names, feature[len(arg_field_names):-1]))
            new_value = function(*args, **kwargs)
            if same_value(old_value, new_value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow(feature[:-1] + [new_value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{new_value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Example #8
0
def update_by_field(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    source_field_name: str,
    dataset_where_sql: Optional[str] = None,
    spatial_reference_item: SpatialReferenceSourceItem = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values with values from another field.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        source_field_name: Name of field to get values from.
        dataset_where_sql: SQL where-clause for dataset subselection.
        spatial_reference_item: Item from which the spatial reference for any geometry
            properties will be set to. If set to None, will use spatial reference of
            the dataset.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by field `%s`.",
        dataset_path,
        field_name,
        source_field_name,
    )
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=[field_name, source_field_name],
        where_clause=dataset_where_sql,
        spatial_reference=SpatialReference(spatial_reference_item).object,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for old_value, new_value in cursor:
            if same_value(old_value, new_value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow([new_value, new_value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{new_value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states