mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-11 17:08:23 +00:00
feat(SDK) Add FormPatchBuilder in python sdk and provide sample CRUD files (#10821)
This commit is contained in:
parent
f9d53b48a6
commit
6745dfb45e
@ -8,6 +8,7 @@ import static com.linkedin.metadata.Constants.DATA_JOB_INFO_ASPECT_NAME;
|
|||||||
import static com.linkedin.metadata.Constants.DATA_JOB_INPUT_OUTPUT_ASPECT_NAME;
|
import static com.linkedin.metadata.Constants.DATA_JOB_INPUT_OUTPUT_ASPECT_NAME;
|
||||||
import static com.linkedin.metadata.Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME;
|
import static com.linkedin.metadata.Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME;
|
||||||
import static com.linkedin.metadata.Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME;
|
import static com.linkedin.metadata.Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME;
|
||||||
|
import static com.linkedin.metadata.Constants.FORM_INFO_ASPECT_NAME;
|
||||||
import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
|
import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
|
||||||
import static com.linkedin.metadata.Constants.GLOSSARY_TERMS_ASPECT_NAME;
|
import static com.linkedin.metadata.Constants.GLOSSARY_TERMS_ASPECT_NAME;
|
||||||
import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME;
|
import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME;
|
||||||
@ -46,7 +47,8 @@ public class AspectTemplateEngine {
|
|||||||
DATA_JOB_INPUT_OUTPUT_ASPECT_NAME,
|
DATA_JOB_INPUT_OUTPUT_ASPECT_NAME,
|
||||||
CHART_INFO_ASPECT_NAME,
|
CHART_INFO_ASPECT_NAME,
|
||||||
DASHBOARD_INFO_ASPECT_NAME,
|
DASHBOARD_INFO_ASPECT_NAME,
|
||||||
STRUCTURED_PROPERTIES_ASPECT_NAME)
|
STRUCTURED_PROPERTIES_ASPECT_NAME,
|
||||||
|
FORM_INFO_ASPECT_NAME)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
private final Map<String, Template<? extends RecordTemplate>> _aspectTemplateMap;
|
private final Map<String, Template<? extends RecordTemplate>> _aspectTemplateMap;
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.linkedin.metadata.aspect.patch.template.form;
|
||||||
|
|
||||||
|
import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.linkedin.data.template.RecordTemplate;
|
||||||
|
import com.linkedin.form.FormInfo;
|
||||||
|
import com.linkedin.metadata.aspect.patch.template.CompoundKeyTemplate;
|
||||||
|
import java.util.Collections;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class FormInfoTemplate extends CompoundKeyTemplate<FormInfo> {
|
||||||
|
|
||||||
|
private static final String PROMPTS_FIELD_NAME = "prompts";
|
||||||
|
private static final String PROMPT_ID_FIELD_NAME = "id";
|
||||||
|
private static final String ACTORS_FIELD_NAME = "actors";
|
||||||
|
private static final String USERS_FIELD_NAME = "users";
|
||||||
|
private static final String GROUPS_FIELD_NAME = "groups";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormInfo getSubtype(RecordTemplate recordTemplate) throws ClassCastException {
|
||||||
|
if (recordTemplate instanceof FormInfo) {
|
||||||
|
return (FormInfo) recordTemplate;
|
||||||
|
}
|
||||||
|
throw new ClassCastException("Unable to cast RecordTemplate to FormInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<FormInfo> getTemplateType() {
|
||||||
|
return FormInfo.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public FormInfo getDefault() {
|
||||||
|
FormInfo formInfo = new FormInfo();
|
||||||
|
formInfo.setName("");
|
||||||
|
|
||||||
|
return formInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public JsonNode transformFields(JsonNode baseNode) {
|
||||||
|
JsonNode transformedNode =
|
||||||
|
arrayFieldToMap(
|
||||||
|
baseNode, PROMPTS_FIELD_NAME, Collections.singletonList(PROMPT_ID_FIELD_NAME));
|
||||||
|
|
||||||
|
JsonNode actors = transformedNode.get(ACTORS_FIELD_NAME);
|
||||||
|
if (actors == null) {
|
||||||
|
actors = instance.objectNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode transformedActorsNode =
|
||||||
|
arrayFieldToMap(actors, USERS_FIELD_NAME, Collections.emptyList());
|
||||||
|
transformedActorsNode =
|
||||||
|
arrayFieldToMap(transformedActorsNode, GROUPS_FIELD_NAME, Collections.emptyList());
|
||||||
|
((ObjectNode) transformedNode).set(ACTORS_FIELD_NAME, transformedActorsNode);
|
||||||
|
|
||||||
|
return transformedNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public JsonNode rebaseFields(JsonNode patched) {
|
||||||
|
JsonNode transformedNode =
|
||||||
|
transformedMapToArray(
|
||||||
|
patched, PROMPTS_FIELD_NAME, Collections.singletonList(PROMPT_ID_FIELD_NAME));
|
||||||
|
|
||||||
|
JsonNode actors = transformedNode.get(ACTORS_FIELD_NAME);
|
||||||
|
if (actors == null) {
|
||||||
|
actors = instance.objectNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode transformedActorsNode =
|
||||||
|
transformedMapToArray(actors, USERS_FIELD_NAME, Collections.emptyList());
|
||||||
|
transformedActorsNode =
|
||||||
|
transformedMapToArray(transformedActorsNode, GROUPS_FIELD_NAME, Collections.emptyList());
|
||||||
|
((ObjectNode) transformedNode).set(ACTORS_FIELD_NAME, transformedActorsNode);
|
||||||
|
|
||||||
|
return transformedNode;
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import com.linkedin.metadata.aspect.patch.template.dataproduct.DataProductProper
|
|||||||
import com.linkedin.metadata.aspect.patch.template.dataset.DatasetPropertiesTemplate;
|
import com.linkedin.metadata.aspect.patch.template.dataset.DatasetPropertiesTemplate;
|
||||||
import com.linkedin.metadata.aspect.patch.template.dataset.EditableSchemaMetadataTemplate;
|
import com.linkedin.metadata.aspect.patch.template.dataset.EditableSchemaMetadataTemplate;
|
||||||
import com.linkedin.metadata.aspect.patch.template.dataset.UpstreamLineageTemplate;
|
import com.linkedin.metadata.aspect.patch.template.dataset.UpstreamLineageTemplate;
|
||||||
|
import com.linkedin.metadata.aspect.patch.template.form.FormInfoTemplate;
|
||||||
import com.linkedin.metadata.models.AspectSpec;
|
import com.linkedin.metadata.models.AspectSpec;
|
||||||
import com.linkedin.metadata.models.DefaultEntitySpec;
|
import com.linkedin.metadata.models.DefaultEntitySpec;
|
||||||
import com.linkedin.metadata.models.EntitySpec;
|
import com.linkedin.metadata.models.EntitySpec;
|
||||||
@ -87,6 +88,7 @@ public class SnapshotEntityRegistry implements EntityRegistry {
|
|||||||
aspectSpecTemplateMap.put(DATA_JOB_INPUT_OUTPUT_ASPECT_NAME, new DataJobInputOutputTemplate());
|
aspectSpecTemplateMap.put(DATA_JOB_INPUT_OUTPUT_ASPECT_NAME, new DataJobInputOutputTemplate());
|
||||||
aspectSpecTemplateMap.put(
|
aspectSpecTemplateMap.put(
|
||||||
STRUCTURED_PROPERTIES_ASPECT_NAME, new StructuredPropertiesTemplate());
|
STRUCTURED_PROPERTIES_ASPECT_NAME, new StructuredPropertiesTemplate());
|
||||||
|
aspectSpecTemplateMap.put(FORM_INFO_ASPECT_NAME, new FormInfoTemplate());
|
||||||
return new AspectTemplateEngine(aspectSpecTemplateMap);
|
return new AspectTemplateEngine(aspectSpecTemplateMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
56
metadata-ingestion/examples/library/create_form.py
Normal file
56
metadata-ingestion/examples/library/create_form.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from datahub.emitter.mcp import MetadataChangeProposalWrapper
|
||||||
|
from datahub.emitter.rest_emitter import DatahubRestEmitter
|
||||||
|
|
||||||
|
# Imports for metadata model classes
|
||||||
|
from datahub.metadata.schema_classes import (
|
||||||
|
FormActorAssignmentClass,
|
||||||
|
FormInfoClass,
|
||||||
|
FormPromptClass,
|
||||||
|
FormPromptTypeClass,
|
||||||
|
FormTypeClass,
|
||||||
|
StructuredPropertyParamsClass,
|
||||||
|
)
|
||||||
|
from datahub.metadata.urns import FormUrn
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
# define the prompts for our form
|
||||||
|
prompt_1 = FormPromptClass(
|
||||||
|
id="1", # ensure IDs are globally unique
|
||||||
|
title="First Prompt",
|
||||||
|
type=FormPromptTypeClass.STRUCTURED_PROPERTY, # structured property type prompt
|
||||||
|
structuredPropertyParams=StructuredPropertyParamsClass(
|
||||||
|
urn="urn:li:structuredProperty:property1"
|
||||||
|
), # reference existing structured property
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
prompt_2 = FormPromptClass(
|
||||||
|
id="2", # ensure IDs are globally unique
|
||||||
|
title="Second Prompt",
|
||||||
|
type=FormPromptTypeClass.FIELDS_STRUCTURED_PROPERTY, # structured property prompt on dataset schema fields
|
||||||
|
structuredPropertyParams=StructuredPropertyParamsClass(
|
||||||
|
urn="urn:li:structuredProperty:property1"
|
||||||
|
),
|
||||||
|
required=False, # dataset schema fields prompts should not be required
|
||||||
|
)
|
||||||
|
|
||||||
|
form_urn = FormUrn("metadata_initiative_1")
|
||||||
|
form_info_aspect = FormInfoClass(
|
||||||
|
name="Metadata Initiative 2024",
|
||||||
|
description="Please respond to this form for metadata compliance purposes",
|
||||||
|
type=FormTypeClass.VERIFICATION,
|
||||||
|
actors=FormActorAssignmentClass(owners=True),
|
||||||
|
prompts=[prompt_1, prompt_2],
|
||||||
|
)
|
||||||
|
|
||||||
|
event: MetadataChangeProposalWrapper = MetadataChangeProposalWrapper(
|
||||||
|
entityUrn=str(form_urn),
|
||||||
|
aspect=form_info_aspect,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create rest emitter
|
||||||
|
rest_emitter = DatahubRestEmitter(gms_server="http://localhost:8080")
|
||||||
|
rest_emitter.emit(event)
|
22
metadata-ingestion/examples/library/delete_form.py
Normal file
22
metadata-ingestion/examples/library/delete_form.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from datahub.ingestion.graph.client import DatahubClientConfig, DataHubGraph
|
||||||
|
from datahub.metadata.urns import FormUrn
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
graph = DataHubGraph(
|
||||||
|
config=DatahubClientConfig(
|
||||||
|
server="http://localhost:8080",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
form_urn = FormUrn("metadata_initiative_1")
|
||||||
|
|
||||||
|
# Hard delete the form
|
||||||
|
graph.delete_entity(urn=str(form_urn), hard=True)
|
||||||
|
# Delete references to this form (must do)
|
||||||
|
graph.delete_references_to_urn(urn=str(form_urn), dry_run=False)
|
||||||
|
|
||||||
|
log.info(f"Deleted form {form_urn}")
|
78
metadata-ingestion/examples/library/update_form.py
Normal file
78
metadata-ingestion/examples/library/update_form.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from datahub.configuration.kafka import KafkaProducerConnectionConfig
|
||||||
|
from datahub.emitter.kafka_emitter import DatahubKafkaEmitter, KafkaEmitterConfig
|
||||||
|
from datahub.emitter.rest_emitter import DataHubRestEmitter
|
||||||
|
from datahub.metadata.schema_classes import (
|
||||||
|
FormPromptClass,
|
||||||
|
FormPromptTypeClass,
|
||||||
|
FormTypeClass,
|
||||||
|
OwnerClass,
|
||||||
|
OwnershipTypeClass,
|
||||||
|
StructuredPropertyParamsClass,
|
||||||
|
)
|
||||||
|
from datahub.metadata.urns import FormUrn
|
||||||
|
from datahub.specific.form import FormPatchBuilder
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
# Get an emitter, either REST or Kafka, this example shows you both
|
||||||
|
def get_emitter() -> Union[DataHubRestEmitter, DatahubKafkaEmitter]:
|
||||||
|
USE_REST_EMITTER = True
|
||||||
|
if USE_REST_EMITTER:
|
||||||
|
gms_endpoint = "http://localhost:8080"
|
||||||
|
return DataHubRestEmitter(gms_server=gms_endpoint)
|
||||||
|
else:
|
||||||
|
kafka_server = "localhost:9092"
|
||||||
|
schema_registry_url = "http://localhost:8081"
|
||||||
|
return DatahubKafkaEmitter(
|
||||||
|
config=KafkaEmitterConfig(
|
||||||
|
connection=KafkaProducerConnectionConfig(
|
||||||
|
bootstrap=kafka_server, schema_registry_url=schema_registry_url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# input your unique form ID
|
||||||
|
form_urn = FormUrn("metadata_initiative_1")
|
||||||
|
|
||||||
|
# example prompts to add, must reference an existing structured property
|
||||||
|
new_prompt = FormPromptClass(
|
||||||
|
id="abcd",
|
||||||
|
title="title",
|
||||||
|
type=FormPromptTypeClass.STRUCTURED_PROPERTY,
|
||||||
|
structuredPropertyParams=StructuredPropertyParamsClass(
|
||||||
|
"urn:li:structuredProperty:io.acryl.test"
|
||||||
|
),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
new_prompt2 = FormPromptClass(
|
||||||
|
id="1234",
|
||||||
|
title="title",
|
||||||
|
type=FormPromptTypeClass.FIELDS_STRUCTURED_PROPERTY,
|
||||||
|
structuredPropertyParams=StructuredPropertyParamsClass(
|
||||||
|
"urn:li:structuredProperty:io.acryl.test"
|
||||||
|
),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with get_emitter() as emitter:
|
||||||
|
for patch_mcp in (
|
||||||
|
FormPatchBuilder(str(form_urn))
|
||||||
|
.add_owner(
|
||||||
|
OwnerClass(
|
||||||
|
owner="urn:li:corpuser:jdoe", type=OwnershipTypeClass.TECHNICAL_OWNER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.set_name("New Name")
|
||||||
|
.set_description("New description here")
|
||||||
|
.set_type(FormTypeClass.VERIFICATION)
|
||||||
|
.set_ownership_form(True)
|
||||||
|
.add_prompts([new_prompt, new_prompt2])
|
||||||
|
.build()
|
||||||
|
):
|
||||||
|
emitter.emit(patch_mcp)
|
146
metadata-ingestion/src/datahub/specific/form.py
Normal file
146
metadata-ingestion/src/datahub/specific/form.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from datahub.emitter.mcp_patch_builder import MetadataPatchProposal
|
||||||
|
from datahub.metadata.schema_classes import (
|
||||||
|
FormInfoClass as FormInfo,
|
||||||
|
FormPromptClass,
|
||||||
|
KafkaAuditHeaderClass,
|
||||||
|
OwnerClass as Owner,
|
||||||
|
OwnershipTypeClass,
|
||||||
|
SystemMetadataClass,
|
||||||
|
)
|
||||||
|
from datahub.specific.ownership import OwnershipPatchHelper
|
||||||
|
from datahub.utilities.urns.urn import Urn
|
||||||
|
|
||||||
|
|
||||||
|
class FormPatchBuilder(MetadataPatchProposal):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
urn: str,
|
||||||
|
system_metadata: Optional[SystemMetadataClass] = None,
|
||||||
|
audit_header: Optional[KafkaAuditHeaderClass] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
urn, system_metadata=system_metadata, audit_header=audit_header
|
||||||
|
)
|
||||||
|
self.ownership_patch_helper = OwnershipPatchHelper(self)
|
||||||
|
|
||||||
|
def add_owner(self, owner: Owner) -> "FormPatchBuilder":
|
||||||
|
self.ownership_patch_helper.add_owner(owner)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_owner(
|
||||||
|
self, owner: str, owner_type: Optional[OwnershipTypeClass] = None
|
||||||
|
) -> "FormPatchBuilder":
|
||||||
|
"""
|
||||||
|
param: owner_type is optional
|
||||||
|
"""
|
||||||
|
self.ownership_patch_helper.remove_owner(owner, owner_type)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_owners(self, owners: List[Owner]) -> "FormPatchBuilder":
|
||||||
|
self.ownership_patch_helper.set_owners(owners)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_name(self, name: Optional[str] = None) -> "FormPatchBuilder":
|
||||||
|
if name is not None:
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path="/name",
|
||||||
|
value=name,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_description(self, description: Optional[str] = None) -> "FormPatchBuilder":
|
||||||
|
if description is not None:
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path="/description",
|
||||||
|
value=description,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_type(self, type: Optional[str] = None) -> "FormPatchBuilder":
|
||||||
|
if type is not None:
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path="/type",
|
||||||
|
value=type,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_prompt(self, prompt: FormPromptClass) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path=f"/prompts/{self.quote(prompt.id)}",
|
||||||
|
value=prompt,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_prompts(self, prompts: List[FormPromptClass]) -> "FormPatchBuilder":
|
||||||
|
for prompt in prompts:
|
||||||
|
self.add_prompt(prompt)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_prompt(self, prompt_id: str) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"remove",
|
||||||
|
path=f"/prompts/{self.quote(prompt_id)}",
|
||||||
|
value=prompt_id,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_prompts(self, prompt_ids: List[str]) -> "FormPatchBuilder":
|
||||||
|
for prompt_id in prompt_ids:
|
||||||
|
self.remove_prompt(prompt_id)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_ownership_form(self, is_ownership: bool) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path="/actors/owners",
|
||||||
|
value=is_ownership,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_assigned_user(self, user_urn: Union[str, Urn]) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path=f"/actors/users/{self.quote(str(user_urn))}",
|
||||||
|
value=user_urn,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_assigned_user(self, user_urn: Union[str, Urn]) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"remove",
|
||||||
|
path=f"/actors/users/{self.quote(str(user_urn))}",
|
||||||
|
value=user_urn,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_assigned_group(self, group_urn: Union[str, Urn]) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"add",
|
||||||
|
path=f"/actors/groups/{self.quote(str(group_urn))}",
|
||||||
|
value=group_urn,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_assigned_group(self, group_urn: Union[str, Urn]) -> "FormPatchBuilder":
|
||||||
|
self._add_patch(
|
||||||
|
FormInfo.ASPECT_NAME,
|
||||||
|
"remove",
|
||||||
|
path=f"/actors/groups/{self.quote(str(group_urn))}",
|
||||||
|
value=group_urn,
|
||||||
|
)
|
||||||
|
return self
|
Loading…
x
Reference in New Issue
Block a user