MINOR - Fix lineage GET for names with / and standardize quote calls (#17748)

* MINOR - Fix lineage GET for names with `/` and standardize quote calls

* format

* fix import
This commit is contained in:
Pere Miquel Brull 2024-09-06 12:12:44 +02:00
parent fa198f2942
commit f9dd7b53cb
9 changed files with 102 additions and 17 deletions

View File

@ -19,11 +19,11 @@ import traceback
from typing import Generic, Iterable, List, Optional, Set, Type, TypeVar from typing import Generic, Iterable, List, Optional, Set, Type, TypeVar
from pydantic import BaseModel from pydantic import BaseModel
from requests.utils import quote
from metadata.generated.schema.entity.data.container import Container from metadata.generated.schema.entity.data.container import Container
from metadata.generated.schema.entity.data.query import Query from metadata.generated.schema.entity.data.query import Query
from metadata.ingestion.ometa.client import REST, APIError from metadata.ingestion.ometa.client import REST, APIError
from metadata.ingestion.ometa.utils import quote
from metadata.utils.elasticsearch import ES_INDEX_MAP from metadata.utils.elasticsearch import ES_INDEX_MAP
from metadata.utils.logger import ometa_logger from metadata.utils.logger import ometa_logger

View File

@ -28,7 +28,7 @@ from metadata.ingestion.lineage.models import ConnectionTypeDialectMapper
from metadata.ingestion.lineage.parser import LINEAGE_PARSING_TIMEOUT from metadata.ingestion.lineage.parser import LINEAGE_PARSING_TIMEOUT
from metadata.ingestion.models.patch_request import build_patch from metadata.ingestion.models.patch_request import build_patch
from metadata.ingestion.ometa.client import REST, APIError from metadata.ingestion.ometa.client import REST, APIError
from metadata.ingestion.ometa.utils import get_entity_type from metadata.ingestion.ometa.utils import get_entity_type, quote
from metadata.utils.logger import ometa_logger from metadata.utils.logger import ometa_logger
from metadata.utils.lru_cache import LRU_CACHE_SIZE, LRUCache from metadata.utils.lru_cache import LRU_CACHE_SIZE, LRUCache
@ -279,7 +279,7 @@ class OMetaLineageMixin(Generic[T]):
""" """
return self._get_lineage( return self._get_lineage(
entity=entity, entity=entity,
path=f"name/{fqn}", path=f"name/{quote(fqn)}",
up_depth=up_depth, up_depth=up_depth,
down_depth=down_depth, down_depth=down_depth,
) )

View File

@ -16,8 +16,7 @@ To be used by OpenMetadata class
import traceback import traceback
from typing import List, Optional, Type, TypeVar from typing import List, Optional, Type, TypeVar
from pydantic import BaseModel from pydantic import BaseModel, validate_call
from requests.utils import quote
from metadata.generated.schema.api.data.createTableProfile import ( from metadata.generated.schema.api.data.createTableProfile import (
CreateTableProfileRequest, CreateTableProfileRequest,
@ -39,7 +38,7 @@ from metadata.generated.schema.type.basic import FullyQualifiedEntityName, Uuid
from metadata.generated.schema.type.usageRequest import UsageRequest from metadata.generated.schema.type.usageRequest import UsageRequest
from metadata.ingestion.ometa.client import REST from metadata.ingestion.ometa.client import REST
from metadata.ingestion.ometa.models import EntityList from metadata.ingestion.ometa.models import EntityList
from metadata.ingestion.ometa.utils import model_str from metadata.ingestion.ometa.utils import model_str, quote
from metadata.utils.logger import ometa_logger from metadata.utils.logger import ometa_logger
logger = ometa_logger() logger = ometa_logger()
@ -227,6 +226,7 @@ class OMetaTableMixin:
return None return None
@validate_call
def get_profile_data( def get_profile_data(
self, self,
fqn: str, fqn: str,
@ -253,7 +253,6 @@ class OMetaTableMixin:
Returns: Returns:
EntityList: EntityList list object EntityList: EntityList list object
""" """
url_after = f"&after={after}" if after else "" url_after = f"&after={after}" if after else ""
profile_type_url = profile_type.__name__[0].lower() + profile_type.__name__[1:] profile_type_url = profile_type.__name__[0].lower() + profile_type.__name__[1:]
@ -290,7 +289,7 @@ class OMetaTableMixin:
Returns: Returns:
Optional[Table]: OM table object Optional[Table]: OM table object
""" """
return self._get(Table, f"{quote(model_str(fqn), safe='')}/tableProfile/latest") return self._get(Table, f"{quote(fqn)}/tableProfile/latest")
def create_or_update_custom_metric( def create_or_update_custom_metric(
self, custom_metric: CreateCustomMetricRequest, table_id: str self, custom_metric: CreateCustomMetricRequest, table_id: str

View File

@ -17,7 +17,6 @@ To be used by OpenMetadata class
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import List, Optional, Type, Union from typing import List, Optional, Type, Union
from urllib.parse import quote
from uuid import UUID from uuid import UUID
from metadata.generated.schema.api.tests.createLogicalTestCases import ( from metadata.generated.schema.api.tests.createLogicalTestCases import (
@ -46,7 +45,7 @@ from metadata.generated.schema.tests.testDefinition import (
from metadata.generated.schema.tests.testSuite import TestSuite from metadata.generated.schema.tests.testSuite import TestSuite
from metadata.generated.schema.type.entityReference import EntityReference from metadata.generated.schema.type.entityReference import EntityReference
from metadata.ingestion.ometa.client import REST from metadata.ingestion.ometa.client import REST
from metadata.ingestion.ometa.utils import model_str from metadata.ingestion.ometa.utils import model_str, quote
from metadata.utils.logger import ometa_logger from metadata.utils.logger import ometa_logger
logger = ometa_logger() logger = ometa_logger()
@ -76,7 +75,7 @@ class OMetaTestsMixin:
_type_: _description_ _type_: _description_
""" """
resp = self.client.put( resp = self.client.put(
f"{self.get_suffix(TestCase)}/{quote(test_case_fqn,safe='')}/testCaseResult", f"{self.get_suffix(TestCase)}/{quote(test_case_fqn)}/testCaseResult",
test_results.model_dump_json(), test_results.model_dump_json(),
) )

View File

@ -18,7 +18,6 @@ import traceback
from typing import Dict, Generic, Iterable, List, Optional, Type, TypeVar, Union from typing import Dict, Generic, Iterable, List, Optional, Type, TypeVar, Union
from pydantic import BaseModel from pydantic import BaseModel
from requests.utils import quote
from metadata.generated.schema.api.services.ingestionPipelines.createIngestionPipeline import ( from metadata.generated.schema.api.services.ingestionPipelines.createIngestionPipeline import (
CreateIngestionPipelineRequest, CreateIngestionPipelineRequest,
@ -57,7 +56,7 @@ from metadata.ingestion.ometa.mixins.user_mixin import OMetaUserMixin
from metadata.ingestion.ometa.mixins.version_mixin import OMetaVersionMixin from metadata.ingestion.ometa.mixins.version_mixin import OMetaVersionMixin
from metadata.ingestion.ometa.models import EntityList from metadata.ingestion.ometa.models import EntityList
from metadata.ingestion.ometa.routes import ROUTES from metadata.ingestion.ometa.routes import ROUTES
from metadata.ingestion.ometa.utils import get_entity_type, model_str from metadata.ingestion.ometa.utils import get_entity_type, model_str, quote
from metadata.utils.logger import ometa_logger from metadata.utils.logger import ometa_logger
from metadata.utils.secrets.secrets_manager_factory import SecretsManagerFactory from metadata.utils.secrets.secrets_manager_factory import SecretsManagerFactory
from metadata.utils.ssl_registry import get_verify_ssl_fn from metadata.utils.ssl_registry import get_verify_ssl_fn
@ -293,7 +292,7 @@ class OpenMetadata(
return self._get( return self._get(
entity=entity, entity=entity,
path=f"name/{quote(model_str(fqn), safe='')}", path=f"name/{quote(fqn)}",
fields=fields, fields=fields,
nullable=nullable, nullable=nullable,
) )

View File

@ -17,6 +17,9 @@ import string
from typing import Any, Type, TypeVar, Union from typing import Any, Type, TypeVar, Union
from pydantic import BaseModel from pydantic import BaseModel
from requests.utils import quote as url_quote
from metadata.generated.schema.type.basic import FullyQualifiedEntityName
T = TypeVar("T", bound=BaseModel) T = TypeVar("T", bound=BaseModel)
@ -74,3 +77,11 @@ def model_str(arg: Any) -> str:
return str(arg.root) return str(arg.root)
return str(arg) return str(arg)
def quote(fqn: Union[FullyQualifiedEntityName, str]) -> str:
"""
Quote the FQN so that it's safe to pass to the API.
E.g., `"foo.bar/baz"` -> `%22foo.bar%2Fbaz%22`
"""
return url_quote(model_str(fqn), safe="")

View File

@ -25,6 +25,7 @@ from metadata.generated.schema.entity.data.table import Table
from metadata.generated.schema.entity.services.dashboardService import DashboardService from metadata.generated.schema.entity.services.dashboardService import DashboardService
from metadata.generated.schema.entity.services.databaseService import DatabaseService from metadata.generated.schema.entity.services.databaseService import DatabaseService
from metadata.generated.schema.entity.services.pipelineService import PipelineService from metadata.generated.schema.entity.services.pipelineService import PipelineService
from metadata.generated.schema.type.basic import EntityName
from metadata.generated.schema.type.entityLineage import ( from metadata.generated.schema.type.entityLineage import (
ColumnLineage, ColumnLineage,
EntitiesEdge, EntitiesEdge,
@ -85,7 +86,7 @@ class OMetaLineageTest(TestCase):
) )
) )
create_schema_entity = cls.metadata.create_or_update( cls.create_schema_entity = cls.metadata.create_or_update(
data=get_create_entity( data=get_create_entity(
entity=DatabaseSchema, entity=DatabaseSchema,
reference=create_db_entity.fullyQualifiedName, reference=create_db_entity.fullyQualifiedName,
@ -96,14 +97,14 @@ class OMetaLineageTest(TestCase):
cls.table1 = get_create_entity( cls.table1 = get_create_entity(
name=generate_name(), name=generate_name(),
entity=Table, entity=Table,
reference=create_schema_entity.fullyQualifiedName, reference=cls.create_schema_entity.fullyQualifiedName,
) )
cls.table1_entity = cls.metadata.create_or_update(data=cls.table1) cls.table1_entity = cls.metadata.create_or_update(data=cls.table1)
cls.table2 = get_create_entity( cls.table2 = get_create_entity(
name=generate_name(), name=generate_name(),
entity=Table, entity=Table,
reference=create_schema_entity.fullyQualifiedName, reference=cls.create_schema_entity.fullyQualifiedName,
) )
cls.table2_entity = cls.metadata.create_or_update(data=cls.table2) cls.table2_entity = cls.metadata.create_or_update(data=cls.table2)
@ -337,3 +338,50 @@ class OMetaLineageTest(TestCase):
) )
entity_lineage = EntityLineage.model_validate(datamodel_lineage) entity_lineage = EntityLineage.model_validate(datamodel_lineage)
self.assertEqual(from_id, str(entity_lineage.upstreamEdges[0].fromEntity.root)) self.assertEqual(from_id, str(entity_lineage.upstreamEdges[0].fromEntity.root))
def test_table_with_slash_in_name(self):
"""E.g., `foo.bar/baz`"""
name = EntityName("foo.bar/baz")
new_table: Table = self.metadata.create_or_update(
data=get_create_entity(
entity=Table,
name=name,
reference=self.create_schema_entity.fullyQualifiedName,
)
)
res: Table = self.metadata.get_by_name(
entity=Table, fqn=new_table.fullyQualifiedName
)
assert res.name == name
self.metadata.add_lineage(
data=AddLineageRequest(
edge=EntitiesEdge(
fromEntity=EntityReference(id=self.table1_entity.id, type="table"),
toEntity=EntityReference(id=new_table.id, type="table"),
lineageDetails=LineageDetails(
columnsLineage=[
ColumnLineage(
fromColumns=[
self.table1_entity.columns[0].fullyQualifiedName
],
toColumn=new_table.columns[0].fullyQualifiedName,
)
]
),
),
)
)
# use the SDK to get the lineage
lineage = self.metadata.get_lineage_by_name(
entity=Table,
fqn=new_table.fullyQualifiedName.root,
)
entity_lineage = EntityLineage.model_validate(lineage)
assert (
entity_lineage.upstreamEdges[0].fromEntity.root
== self.table1_entity.id.root
)

View File

@ -76,6 +76,8 @@ from metadata.generated.schema.type.entityReferenceList import EntityReferenceLi
from metadata.generated.schema.type.usageRequest import UsageRequest from metadata.generated.schema.type.usageRequest import UsageRequest
from metadata.ingestion.ometa.client import REST from metadata.ingestion.ometa.client import REST
from ..integration_base import get_create_entity
BAD_RESPONSE = { BAD_RESPONSE = {
"data": [ "data": [
{ {
@ -643,3 +645,20 @@ class OMetaTableTest(TestCase):
# We should have 2 tables, the 3rd one is broken and should be skipped # We should have 2 tables, the 3rd one is broken and should be skipped
assert len(list(res)) == 2 assert len(list(res)) == 2
def test_table_with_slash_in_name(self):
"""E.g., `foo.bar/baz`"""
name = EntityName("foo.bar/baz")
new_table: Table = self.metadata.create_or_update(
data=get_create_entity(
entity=Table,
name=name,
reference=self.create_schema_entity.fullyQualifiedName,
)
)
res: Table = self.metadata.get_by_name(
entity=Table, fqn=new_table.fullyQualifiedName
)
assert res.name == name

View File

@ -17,6 +17,8 @@ from unittest.mock import MagicMock
import pytest import pytest
from metadata.generated.schema.entity.data.table import Table from metadata.generated.schema.entity.data.table import Table
from metadata.generated.schema.type.basic import FullyQualifiedEntityName
from metadata.ingestion.ometa.utils import quote
from metadata.utils import fqn from metadata.utils import fqn
@ -145,3 +147,11 @@ class TestFqn(TestCase):
with pytest.raises(ValueError): with pytest.raises(ValueError):
fqn.split_test_case_fqn("local_redshift.dev.dbt_jaffle.customers") fqn.split_test_case_fqn("local_redshift.dev.dbt_jaffle.customers")
def test_quote_fqns(self):
"""We can properly quote FQNs for URL usage"""
assert quote(FullyQualifiedEntityName("a.b.c")) == "a.b.c"
# Works with strings directly
assert quote("a.b.c") == "a.b.c"
assert quote(FullyQualifiedEntityName('"foo.bar".baz')) == "%22foo.bar%22.baz"
assert quote('"foo.bar/baz".hello') == "%22foo.bar%2Fbaz%22.hello"