data-product-portal-plugins 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data_product_portal_plugins-0.8.0/PKG-INFO +15 -0
- data_product_portal_plugins-0.8.0/portal_plugins/agno/agno-logo.svg +4 -0
- data_product_portal_plugins-0.8.0/portal_plugins/agno/schema.py +33 -0
- data_product_portal_plugins-0.8.0/portal_plugins/azure_blob/azure-storage-account-logo.svg +22 -0
- data_product_portal_plugins-0.8.0/portal_plugins/azure_blob/model.py +20 -0
- data_product_portal_plugins-0.8.0/portal_plugins/azure_blob/schema.py +119 -0
- data_product_portal_plugins-0.8.0/portal_plugins/azure_blob/versions/azure_blob_0001_baseline.py +37 -0
- data_product_portal_plugins-0.8.0/portal_plugins/conveyor/conveyor-logo.svg +17 -0
- data_product_portal_plugins-0.8.0/portal_plugins/conveyor/schema.py +35 -0
- data_product_portal_plugins-0.8.0/portal_plugins/databricks/databricks-logo.svg +14 -0
- data_product_portal_plugins-0.8.0/portal_plugins/databricks/model.py +22 -0
- data_product_portal_plugins-0.8.0/portal_plugins/databricks/schema.py +151 -0
- data_product_portal_plugins-0.8.0/portal_plugins/databricks/versions/databricks_0001_baseline.py +41 -0
- data_product_portal_plugins-0.8.0/portal_plugins/github/github-logo.svg +3 -0
- data_product_portal_plugins-0.8.0/portal_plugins/github/schema.py +35 -0
- data_product_portal_plugins-0.8.0/portal_plugins/glue/glue-logo.svg +18 -0
- data_product_portal_plugins-0.8.0/portal_plugins/glue/mcp_instructions.py +42 -0
- data_product_portal_plugins-0.8.0/portal_plugins/glue/mcp_tools.py +397 -0
- data_product_portal_plugins-0.8.0/portal_plugins/glue/model.py +22 -0
- data_product_portal_plugins-0.8.0/portal_plugins/glue/schema.py +153 -0
- data_product_portal_plugins-0.8.0/portal_plugins/glue/versions/glue_0001_baseline.py +41 -0
- data_product_portal_plugins-0.8.0/portal_plugins/osi_sem_model/model.py +17 -0
- data_product_portal_plugins-0.8.0/portal_plugins/osi_sem_model/osi-logo.svg +28 -0
- data_product_portal_plugins-0.8.0/portal_plugins/osi_sem_model/schema.py +69 -0
- data_product_portal_plugins-0.8.0/portal_plugins/osi_sem_model/versions/osi_sem_model_0001_baseline.py +36 -0
- data_product_portal_plugins-0.8.0/portal_plugins/parameter_store/model.py +17 -0
- data_product_portal_plugins-0.8.0/portal_plugins/parameter_store/parameter-store-logo.svg +1 -0
- data_product_portal_plugins-0.8.0/portal_plugins/parameter_store/schema.py +106 -0
- data_product_portal_plugins-0.8.0/portal_plugins/parameter_store/versions/parameter_store_0001_baseline.py +36 -0
- data_product_portal_plugins-0.8.0/portal_plugins/postgresql/model.py +19 -0
- data_product_portal_plugins-0.8.0/portal_plugins/postgresql/postgresql-logo.svg +2 -0
- data_product_portal_plugins-0.8.0/portal_plugins/postgresql/schema.py +136 -0
- data_product_portal_plugins-0.8.0/portal_plugins/postgresql/versions/postgresql_0001_baseline.py +38 -0
- data_product_portal_plugins-0.8.0/portal_plugins/redshift/aws-redshift-logo.svg +1 -0
- data_product_portal_plugins-0.8.0/portal_plugins/redshift/model.py +22 -0
- data_product_portal_plugins-0.8.0/portal_plugins/redshift/schema.py +144 -0
- data_product_portal_plugins-0.8.0/portal_plugins/redshift/versions/redshift_0001_baseline.py +41 -0
- data_product_portal_plugins-0.8.0/portal_plugins/s3/model.py +18 -0
- data_product_portal_plugins-0.8.0/portal_plugins/s3/s3-logo.svg +1 -0
- data_product_portal_plugins-0.8.0/portal_plugins/s3/schema.py +103 -0
- data_product_portal_plugins-0.8.0/portal_plugins/s3/versions/s3_0001_baseline.py +37 -0
- data_product_portal_plugins-0.8.0/portal_plugins/snowflake/model.py +22 -0
- data_product_portal_plugins-0.8.0/portal_plugins/snowflake/schema.py +151 -0
- data_product_portal_plugins-0.8.0/portal_plugins/snowflake/snowflake-logo.svg +8 -0
- data_product_portal_plugins-0.8.0/portal_plugins/snowflake/versions/snowflake_0001_baseline.py +41 -0
- data_product_portal_plugins-0.8.0/pyproject.toml +43 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: data-product-portal-plugins
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: First-party technical asset plugins for Data Product Portal
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Requires-Dist: alembic (>=1.20.0)
|
|
10
|
+
Requires-Dist: boto3 (>=1.43.93)
|
|
11
|
+
Requires-Dist: data-product-portal (>=0.8.0)
|
|
12
|
+
Requires-Dist: fastapi (>=0.141.1)
|
|
13
|
+
Requires-Dist: fastmcp (>=3.4.7)
|
|
14
|
+
Requires-Dist: pydantic (>=2.13.5)
|
|
15
|
+
Requires-Dist: sqlalchemy (>=2.0.52)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="1em" height="1em" viewBox="16 9 56 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M52.4943 9.29102H29.5019V17.2933H46.8809L61.6912 56.3333H71.2585L52.4943 9.29102Z" fill="#ff4017"/>
|
|
3
|
+
<path d="M40.0389 48.3312H16.4961V56.3334H40.0389V48.3312Z" fill="#ff4017"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import ClassVar, Optional
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from app.data_products.model import DataProduct as DataProductModel
|
|
7
|
+
from app.technical_asset_configuration.base_schema import (
|
|
8
|
+
PlatformMetadata,
|
|
9
|
+
TechnicalAssetPlugin,
|
|
10
|
+
)
|
|
11
|
+
from app.users.schema import User
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgnoPlugin(TechnicalAssetPlugin):
|
|
15
|
+
name: ClassVar[str] = "AgnoPlugin"
|
|
16
|
+
version: ClassVar[str] = "1.0"
|
|
17
|
+
|
|
18
|
+
_platform_metadata = PlatformMetadata(
|
|
19
|
+
display_name="Agno",
|
|
20
|
+
icon_name="agno-logo.svg",
|
|
21
|
+
icon_package="portal_plugins.agno",
|
|
22
|
+
platform_key="agno",
|
|
23
|
+
has_environments=False,
|
|
24
|
+
show_in_form=False,
|
|
25
|
+
detailed_name="Agno Agent",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_url(
|
|
30
|
+
cls, id: UUID, db: Session, actor: User, environment: Optional[str] = None
|
|
31
|
+
) -> str:
|
|
32
|
+
data_product = db.get(DataProductModel, id)
|
|
33
|
+
return f"https://os.agno.com/chat?type=agent&id={data_product.namespace}-agent"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg id="f2f04349-8aee-4413-84c9-a9053611b319" xmlns="http://www.w3.org/2000/svg" width="18" height="18"
|
|
2
|
+
viewBox="0 0 18 18">
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="ad4c4f96-09aa-4f91-ba10-5cb8ad530f74" x1="9" y1="15.83" x2="9" y2="5.79"
|
|
5
|
+
gradientUnits="userSpaceOnUse">
|
|
6
|
+
<stop offset="0" stop-color="#b3b3b3"/>
|
|
7
|
+
<stop offset="0.26" stop-color="#c1c1c1"/>
|
|
8
|
+
<stop offset="1" stop-color="#e6e6e6"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<title>Icon-storage-86</title>
|
|
12
|
+
<path d="M.5,5.79h17a0,0,0,0,1,0,0v9.48a.57.57,0,0,1-.57.57H1.07a.57.57,0,0,1-.57-.57V5.79A0,0,0,0,1,.5,5.79Z"
|
|
13
|
+
fill="url(#ad4c4f96-09aa-4f91-ba10-5cb8ad530f74)"/>
|
|
14
|
+
<path d="M1.07,2.17H16.93a.57.57,0,0,1,.57.57V5.79a0,0,0,0,1,0,0H.5a0,0,0,0,1,0,0V2.73A.57.57,0,0,1,1.07,2.17Z"
|
|
15
|
+
fill="#37c2b1"/>
|
|
16
|
+
<path d="M2.81,6.89H15.18a.27.27,0,0,1,.26.27v1.4a.27.27,0,0,1-.26.27H2.81a.27.27,0,0,1-.26-.27V7.16A.27.27,0,0,1,2.81,6.89Z"
|
|
17
|
+
fill="#fff"/>
|
|
18
|
+
<path d="M2.82,9.68H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V10A.27.27,0,0,1,2.82,9.68Z"
|
|
19
|
+
fill="#37c2b1"/>
|
|
20
|
+
<path d="M2.82,12.5H15.19a.27.27,0,0,1,.26.27v1.41a.27.27,0,0,1-.26.27H2.82a.27.27,0,0,1-.26-.27V12.77A.27.27,0,0,1,2.82,12.5Z"
|
|
21
|
+
fill="#258277"/>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from sqlalchemy import String
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from app.technical_asset_configuration.base_model import BaseTechnicalAssetConfiguration
|
|
5
|
+
|
|
6
|
+
NAME = "AzureBlobTechnicalAssetConfiguration"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AzureBlobTechnicalAssetConfiguration(BaseTechnicalAssetConfiguration):
|
|
10
|
+
__tablename__ = "azure_blob_technical_asset_configurations"
|
|
11
|
+
|
|
12
|
+
domain: Mapped[str] = mapped_column(
|
|
13
|
+
String, nullable=False
|
|
14
|
+
) # Required to support looking up the correct storage account
|
|
15
|
+
path: Mapped[str] = mapped_column(String, nullable=True)
|
|
16
|
+
container_name: Mapped[str] = mapped_column(String, nullable=False)
|
|
17
|
+
|
|
18
|
+
__mapper_args__ = {
|
|
19
|
+
"polymorphic_identity": NAME,
|
|
20
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import ClassVar, Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
|
|
7
|
+
from app.configuration.environments.platform_service_configurations.schemas import (
|
|
8
|
+
AzureBlobConfig,
|
|
9
|
+
)
|
|
10
|
+
from portal_plugins.azure_blob.model import (
|
|
11
|
+
NAME,
|
|
12
|
+
)
|
|
13
|
+
from portal_plugins.azure_blob.model import (
|
|
14
|
+
AzureBlobTechnicalAssetConfiguration as AzureBlobTechnicalAssetConfigurationModel,
|
|
15
|
+
)
|
|
16
|
+
from app.technical_asset_configuration.base_schema import (
|
|
17
|
+
PluginContext,
|
|
18
|
+
PlatformMetadata,
|
|
19
|
+
TechnicalAssetPlugin,
|
|
20
|
+
UIElementMetadata,
|
|
21
|
+
UIElementString,
|
|
22
|
+
)
|
|
23
|
+
from app.technical_asset_configuration.enums import UIElementType
|
|
24
|
+
from app.users.schema import User
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AzureBlobTechnicalAssetConfiguration(TechnicalAssetPlugin):
|
|
30
|
+
name: ClassVar[str] = NAME
|
|
31
|
+
version: ClassVar[str] = "1.0"
|
|
32
|
+
|
|
33
|
+
domain: str = ""
|
|
34
|
+
path: str = ""
|
|
35
|
+
container_name: str
|
|
36
|
+
|
|
37
|
+
_platform_metadata = PlatformMetadata(
|
|
38
|
+
result_string_template="platform_provided_storage_account/{container_name}/{path}",
|
|
39
|
+
technical_info_template="https://{storage_account}.blob.core.windows.net/{container_name}/{path}",
|
|
40
|
+
display_name="Blob",
|
|
41
|
+
icon_name="azure-storage-account-logo.svg",
|
|
42
|
+
icon_package="portal_plugins.azure_blob",
|
|
43
|
+
platform_key="azureblob",
|
|
44
|
+
parent_platform="azure",
|
|
45
|
+
result_label="Resulting path",
|
|
46
|
+
result_tooltip="The path you can access through this technical asset",
|
|
47
|
+
detailed_name="Path",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
class Meta:
|
|
51
|
+
orm_model = AzureBlobTechnicalAssetConfigurationModel
|
|
52
|
+
|
|
53
|
+
def validate_configuration(self, context: PluginContext, db: Session):
|
|
54
|
+
self.domain = context.domain.name
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_url(
|
|
58
|
+
cls, id: UUID, db: Session, actor: User, environment: Optional[str] = None
|
|
59
|
+
) -> str:
|
|
60
|
+
return "https://portal.azure.com/"
|
|
61
|
+
|
|
62
|
+
def get_configuration(
|
|
63
|
+
self, configs: list[AzureBlobConfig]
|
|
64
|
+
) -> Optional[AzureBlobConfig]:
|
|
65
|
+
"""
|
|
66
|
+
No filtering on blob configuration as there should only be 1 map per environment to translate domain -> storage account
|
|
67
|
+
"""
|
|
68
|
+
return next((config for config in configs), None)
|
|
69
|
+
|
|
70
|
+
def render_template(self, template, **context):
|
|
71
|
+
"""
|
|
72
|
+
Example template: https://{storage_account}.blob.core.windows.net/{container_name}/{path}
|
|
73
|
+
Render the template with the provided context, handling storage account resolution.
|
|
74
|
+
Supports 2 different template formats for resolving the technical information:
|
|
75
|
+
- The platform env configuration contains storage account names by domain. We fetch the storage account name for the domain and use it in the template.
|
|
76
|
+
- There is only one storage account name assigned to the default key in the platform env configuration, we use it in the template.
|
|
77
|
+
"""
|
|
78
|
+
logger.info("Start rendering template")
|
|
79
|
+
if "{storage_account}" in template:
|
|
80
|
+
storage_account_names = context.get("storage_account_names", {})
|
|
81
|
+
if storage_account_names and self.domain in storage_account_names:
|
|
82
|
+
context["storage_account"] = storage_account_names[self.domain]
|
|
83
|
+
logger.info(f"storage account name is {context['storage_account']}")
|
|
84
|
+
elif len(storage_account_names) == 1 and "default" in storage_account_names:
|
|
85
|
+
context["storage_account"] = list(storage_account_names.values())[0]
|
|
86
|
+
logger.info(f"storage account name is {context['storage_account']}")
|
|
87
|
+
return super().render_template(template, **context)
|
|
88
|
+
else:
|
|
89
|
+
return super().render_template(template, **context)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def get_ui_metadata(cls, db: Session) -> list[UIElementMetadata]:
|
|
93
|
+
base_metadata = super().get_ui_metadata(db)
|
|
94
|
+
base_metadata += [
|
|
95
|
+
UIElementMetadata(
|
|
96
|
+
name="domain",
|
|
97
|
+
label="domain",
|
|
98
|
+
type=UIElementType.String,
|
|
99
|
+
required=False,
|
|
100
|
+
hidden=True,
|
|
101
|
+
string=UIElementString(initial_value=""),
|
|
102
|
+
),
|
|
103
|
+
UIElementMetadata(
|
|
104
|
+
name="container_name",
|
|
105
|
+
label="Container",
|
|
106
|
+
required=True,
|
|
107
|
+
type=UIElementType.String,
|
|
108
|
+
string=UIElementString(initial_value=""),
|
|
109
|
+
),
|
|
110
|
+
UIElementMetadata(
|
|
111
|
+
name="path",
|
|
112
|
+
label="Path",
|
|
113
|
+
required=False,
|
|
114
|
+
type=UIElementType.String,
|
|
115
|
+
tooltip="The name of the path to give write access to",
|
|
116
|
+
string=UIElementString(initial_value=""),
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
return base_metadata
|
data_product_portal_plugins-0.8.0/portal_plugins/azure_blob/versions/azure_blob_0001_baseline.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Sequence, Union
|
|
2
|
+
|
|
3
|
+
import sqlalchemy as sa
|
|
4
|
+
from alembic import op
|
|
5
|
+
from sqlalchemy.dialects import postgresql
|
|
6
|
+
|
|
7
|
+
from app.shared.model import utcnow
|
|
8
|
+
|
|
9
|
+
revision: str = "azure_blob_0001_baseline"
|
|
10
|
+
down_revision: Union[str, None] = None
|
|
11
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
12
|
+
depends_on: Union[str, Sequence[str], None] = "5477aa0d86f0"
|
|
13
|
+
|
|
14
|
+
TABLE = "azure_blob_technical_asset_configurations"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def upgrade() -> None:
|
|
18
|
+
op.create_table(
|
|
19
|
+
TABLE,
|
|
20
|
+
sa.Column(
|
|
21
|
+
"id",
|
|
22
|
+
postgresql.UUID(as_uuid=True),
|
|
23
|
+
sa.ForeignKey("data_output_configurations.id", ondelete="CASCADE"),
|
|
24
|
+
primary_key=True,
|
|
25
|
+
),
|
|
26
|
+
sa.Column("domain", sa.String(), nullable=False),
|
|
27
|
+
sa.Column("container_name", sa.String(), nullable=False),
|
|
28
|
+
sa.Column("path", sa.String(), nullable=True),
|
|
29
|
+
sa.Column("created_on", sa.DateTime(timezone=False), server_default=utcnow()),
|
|
30
|
+
sa.Column("updated_on", sa.DateTime(timezone=False), onupdate=utcnow()),
|
|
31
|
+
sa.Column("deleted_at", sa.DateTime(timezone=False), nullable=True),
|
|
32
|
+
if_not_exists=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def downgrade() -> None:
|
|
37
|
+
op.drop_table(TABLE, if_exists=True)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg width="1em" height="1em" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0_662_6260)">
|
|
3
|
+
<path d="M10.0412 7.6997H3.06342C2.12356 7.6997 1.36151 6.94183 1.36151 6.00712C1.36151 5.07242 2.12356 4.31454 3.06342 4.31454H10.0412C11.263 4.31454 12.2537 3.32926 12.2537 2.11418C12.2537 0.899104 11.263 -0.0861816 10.0412 -0.0861816H5.78646C4.56469 -0.0861816 3.57399 0.899104 3.57399 2.11418H4.9355C4.9355 1.64662 5.31633 1.26789 5.78646 1.26789H10.0412C10.5113 1.26789 10.8922 1.64662 10.8922 2.11418C10.8922 2.58173 10.5113 2.96047 10.0412 2.96047H3.06342C1.37153 2.96047 0 4.32449 0 6.00712C0 7.68975 1.37153 9.05374 3.06342 9.05374H10.0412C10.5113 9.05374 10.8922 9.43249 10.8922 9.90003C10.8922 10.3677 10.5113 10.7463 10.0412 10.7463H5.78646C5.31633 10.7463 4.9355 10.3677 4.9355 9.90003H3.57399C3.57399 11.1151 4.56469 12.1004 5.78646 12.1004H10.0412C11.263 12.1004 12.2537 11.1151 12.2537 9.90003C12.2537 8.68499 11.263 7.6997 10.0412 7.6997Z"
|
|
4
|
+
fill="#29006B"/>
|
|
5
|
+
<path d="M3.40368 6.85335C3.87364 6.85335 4.25463 6.47445 4.25463 6.00706C4.25463 5.53967 3.87364 5.16077 3.40368 5.16077C2.93372 5.16077 2.55273 5.53967 2.55273 6.00706C2.55273 6.47445 2.93372 6.85335 3.40368 6.85335Z"
|
|
6
|
+
fill="#29006B"/>
|
|
7
|
+
<path d="M6.63708 6.85335C7.10704 6.85335 7.48803 6.47445 7.48803 6.00706C7.48803 5.53967 7.10704 5.16077 6.63708 5.16077C6.16712 5.16077 5.78613 5.53967 5.78613 6.00706C5.78613 6.47445 6.16712 6.85335 6.63708 6.85335Z"
|
|
8
|
+
fill="#29006B"/>
|
|
9
|
+
<path d="M9.87048 6.85335C10.3404 6.85335 10.7215 6.47445 10.7215 6.00706C10.7215 5.53967 10.3404 5.16077 9.87048 5.16077C9.40054 5.16077 9.01953 5.53967 9.01953 6.00706C9.01953 6.47445 9.40054 6.85335 9.87048 6.85335Z"
|
|
10
|
+
fill="#29006B"/>
|
|
11
|
+
</g>
|
|
12
|
+
<defs>
|
|
13
|
+
<clipPath id="clip0_662_6260">
|
|
14
|
+
<rect width="12" height="12" fill="white"/>
|
|
15
|
+
</clipPath>
|
|
16
|
+
</defs>
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import ClassVar, Optional
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from app.core.conveyor.notebook_builder import CONVEYOR_SERVICE
|
|
7
|
+
from app.data_products.model import DataProduct as DataProductModel
|
|
8
|
+
from app.technical_asset_configuration.base_schema import (
|
|
9
|
+
PlatformMetadata,
|
|
10
|
+
TechnicalAssetPlugin,
|
|
11
|
+
)
|
|
12
|
+
from app.users.schema import User
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConveyorPlugin(TechnicalAssetPlugin):
|
|
16
|
+
name: ClassVar[str] = "ConveyorPlugin"
|
|
17
|
+
version: ClassVar[str] = "1.0"
|
|
18
|
+
|
|
19
|
+
_platform_metadata = PlatformMetadata(
|
|
20
|
+
display_name="Conveyor",
|
|
21
|
+
icon_name="conveyor-logo.svg",
|
|
22
|
+
icon_package="portal_plugins.conveyor",
|
|
23
|
+
platform_key="conveyor",
|
|
24
|
+
parent_platform=None,
|
|
25
|
+
has_environments=False,
|
|
26
|
+
detailed_name="Conveyor",
|
|
27
|
+
show_in_form=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_url(
|
|
32
|
+
cls, id: UUID, db: Session, actor: User, environment: Optional[str] = None
|
|
33
|
+
) -> str:
|
|
34
|
+
data_product = db.get(DataProductModel, id)
|
|
35
|
+
return CONVEYOR_SERVICE.generate_ide_url(data_product.namespace)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
2
|
+
<g clip-path="url(#clip0_854_6322)">
|
|
3
|
+
<path d="M33 -1H0V34H33V-1Z" fill="url(#pattern0_854_6322)"/>
|
|
4
|
+
</g>
|
|
5
|
+
<defs>
|
|
6
|
+
<pattern id="pattern0_854_6322" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
7
|
+
<use xlink:href="#image0_854_6322" transform="matrix(0.00338462 0 0 0.00314562 -1.50536 -0.144402)"/>
|
|
8
|
+
</pattern>
|
|
9
|
+
<clipPath id="clip0_854_6322">
|
|
10
|
+
<rect width="33" height="33" fill="white"/>
|
|
11
|
+
</clipPath>
|
|
12
|
+
<image id="image0_854_6322" width="1200" height="630" xlink:href="data:image/png;base64,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"/>
|
|
13
|
+
</defs>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from sqlalchemy import String
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from app.technical_asset_configuration.base_model import BaseTechnicalAssetConfiguration
|
|
5
|
+
|
|
6
|
+
NAME = "DatabricksTechnicalAssetConfiguration"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabricksTechnicalAssetConfiguration(BaseTechnicalAssetConfiguration):
|
|
10
|
+
__tablename__ = "databricks_technical_asset_configurations"
|
|
11
|
+
|
|
12
|
+
catalog: Mapped[str] = mapped_column(String, nullable=True)
|
|
13
|
+
schema: Mapped[str] = mapped_column(String, nullable=True)
|
|
14
|
+
bucket_identifier: Mapped[str] = mapped_column(String, nullable=True)
|
|
15
|
+
catalog_path: Mapped[str] = mapped_column(String, nullable=True)
|
|
16
|
+
table: Mapped[str] = mapped_column(String, nullable=True)
|
|
17
|
+
table_path: Mapped[str] = mapped_column(String, nullable=True)
|
|
18
|
+
access_granularity: Mapped[str] = mapped_column(String, nullable=True)
|
|
19
|
+
|
|
20
|
+
__mapper_args__ = {
|
|
21
|
+
"polymorphic_identity": NAME,
|
|
22
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import ClassVar, Optional, Self
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException, status
|
|
5
|
+
from pydantic import model_validator
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
|
|
8
|
+
from app.configuration.environments.platform_configurations.service import (
|
|
9
|
+
EnvironmentPlatformConfigurationService,
|
|
10
|
+
)
|
|
11
|
+
from app.configuration.environments.platform_service_configurations.schemas import (
|
|
12
|
+
DatabricksConfig,
|
|
13
|
+
)
|
|
14
|
+
from app.data_products.model import DataProduct as DataProductModel
|
|
15
|
+
from app.technical_asset_configuration.base_schema import (
|
|
16
|
+
PluginContext,
|
|
17
|
+
FieldDependency,
|
|
18
|
+
PlatformMetadata,
|
|
19
|
+
SelectOption,
|
|
20
|
+
TechnicalAssetPlugin,
|
|
21
|
+
UIElementMetadata,
|
|
22
|
+
UIElementRadio,
|
|
23
|
+
UIElementSelect,
|
|
24
|
+
UIElementString,
|
|
25
|
+
)
|
|
26
|
+
from portal_plugins.databricks.model import (
|
|
27
|
+
NAME,
|
|
28
|
+
)
|
|
29
|
+
from portal_plugins.databricks.model import (
|
|
30
|
+
DatabricksTechnicalAssetConfiguration as DatabricksTechnicalAssetConfigurationModel,
|
|
31
|
+
)
|
|
32
|
+
from app.technical_asset_configuration.enums import AccessGranularity, UIElementType
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DatabricksTechnicalAssetConfiguration(TechnicalAssetPlugin):
|
|
36
|
+
name: ClassVar[str] = NAME
|
|
37
|
+
version: ClassVar[str] = "1.0"
|
|
38
|
+
|
|
39
|
+
catalog: str
|
|
40
|
+
schema: str = ""
|
|
41
|
+
table: str = "*"
|
|
42
|
+
bucket_identifier: str = ""
|
|
43
|
+
catalog_path: str = ""
|
|
44
|
+
table_path: str = ""
|
|
45
|
+
access_granularity: AccessGranularity
|
|
46
|
+
|
|
47
|
+
_platform_metadata = PlatformMetadata(
|
|
48
|
+
result_string_template="{catalog}.{schema}.{table}",
|
|
49
|
+
display_name="Databricks",
|
|
50
|
+
icon_name="databricks-logo.svg",
|
|
51
|
+
icon_package="portal_plugins.databricks",
|
|
52
|
+
platform_key="databricks",
|
|
53
|
+
parent_platform=None,
|
|
54
|
+
result_label="Resulting table",
|
|
55
|
+
result_tooltip="The table you can access through this technical asset",
|
|
56
|
+
detailed_name="Schema",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
class Meta:
|
|
60
|
+
orm_model = DatabricksTechnicalAssetConfigurationModel
|
|
61
|
+
|
|
62
|
+
@model_validator(mode="after")
|
|
63
|
+
def validate_paths(self) -> Self:
|
|
64
|
+
if not self.catalog_path:
|
|
65
|
+
self.catalog_path = self.catalog
|
|
66
|
+
if not self.table_path:
|
|
67
|
+
self.table_path = self.table
|
|
68
|
+
if self.access_granularity == AccessGranularity.Schema:
|
|
69
|
+
self.table = "*"
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def validate_configuration(self, context: PluginContext, db: Session):
|
|
73
|
+
# If product aligned
|
|
74
|
+
if not self.catalog.startswith(context.data_product.namespace):
|
|
75
|
+
raise ValueError("Invalid catalog specified")
|
|
76
|
+
|
|
77
|
+
def get_configuration(
|
|
78
|
+
self, configs: list[DatabricksConfig]
|
|
79
|
+
) -> Optional[DatabricksConfig]:
|
|
80
|
+
return next(
|
|
81
|
+
(config for config in configs if config.identifier == self.catalog), None
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_url(cls, id, db, actor, environment=None) -> str:
|
|
86
|
+
platform_config = EnvironmentPlatformConfigurationService(
|
|
87
|
+
db
|
|
88
|
+
).get_env_platform_config(environment, "Databricks")
|
|
89
|
+
data_product = db.get(DataProductModel, id)
|
|
90
|
+
config = json.loads(platform_config)["workspace_urls"]
|
|
91
|
+
if str(data_product.domain_id) not in config:
|
|
92
|
+
raise HTTPException(
|
|
93
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
94
|
+
detail=(
|
|
95
|
+
f"Workspace not configured for domain {data_product.domain.name}"
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
return config[str(data_product.domain_id)]
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_ui_metadata(cls, db: Session) -> list[UIElementMetadata]:
|
|
102
|
+
base_metadata = super().get_ui_metadata(db)
|
|
103
|
+
base_metadata += [
|
|
104
|
+
UIElementMetadata(
|
|
105
|
+
name="catalog",
|
|
106
|
+
label="Catalog",
|
|
107
|
+
type=UIElementType.Select,
|
|
108
|
+
required=True,
|
|
109
|
+
use_namespace_when_not_source_aligned=True,
|
|
110
|
+
select=UIElementSelect(options=cls.get_platform_options(db)),
|
|
111
|
+
),
|
|
112
|
+
UIElementMetadata(
|
|
113
|
+
name="schema",
|
|
114
|
+
type=UIElementType.String,
|
|
115
|
+
label="Schema",
|
|
116
|
+
tooltip="The name of the schema to give write access to. Defaults to data product namespace",
|
|
117
|
+
required=True,
|
|
118
|
+
),
|
|
119
|
+
UIElementMetadata(
|
|
120
|
+
name="access_granularity",
|
|
121
|
+
label="Access granularity",
|
|
122
|
+
type=UIElementType.Radio,
|
|
123
|
+
tooltip="Give write access to the entire schema or a single table",
|
|
124
|
+
required=True,
|
|
125
|
+
radio=UIElementRadio(
|
|
126
|
+
initial_value=AccessGranularity.Schema,
|
|
127
|
+
options=[
|
|
128
|
+
SelectOption(
|
|
129
|
+
label="Schema level", value=AccessGranularity.Schema
|
|
130
|
+
),
|
|
131
|
+
SelectOption(
|
|
132
|
+
label="Table level", value=AccessGranularity.Table
|
|
133
|
+
),
|
|
134
|
+
],
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
UIElementMetadata(
|
|
138
|
+
name="table",
|
|
139
|
+
label="Table",
|
|
140
|
+
type=UIElementType.String,
|
|
141
|
+
tooltip="The name of the table to give write access to",
|
|
142
|
+
required=True,
|
|
143
|
+
string=UIElementString(initial_value="*"),
|
|
144
|
+
depends_on=[
|
|
145
|
+
FieldDependency(
|
|
146
|
+
field_name="access_granularity", value=AccessGranularity.Table
|
|
147
|
+
)
|
|
148
|
+
],
|
|
149
|
+
),
|
|
150
|
+
]
|
|
151
|
+
return base_metadata
|
data_product_portal_plugins-0.8.0/portal_plugins/databricks/versions/databricks_0001_baseline.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Sequence, Union
|
|
2
|
+
|
|
3
|
+
import sqlalchemy as sa
|
|
4
|
+
from alembic import op
|
|
5
|
+
from sqlalchemy.dialects import postgresql
|
|
6
|
+
|
|
7
|
+
from app.shared.model import utcnow
|
|
8
|
+
|
|
9
|
+
revision: str = "databricks_0001_baseline"
|
|
10
|
+
down_revision: Union[str, None] = None
|
|
11
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
12
|
+
depends_on: Union[str, Sequence[str], None] = "5477aa0d86f0"
|
|
13
|
+
|
|
14
|
+
TABLE = "databricks_technical_asset_configurations"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def upgrade() -> None:
|
|
18
|
+
op.create_table(
|
|
19
|
+
TABLE,
|
|
20
|
+
sa.Column(
|
|
21
|
+
"id",
|
|
22
|
+
postgresql.UUID(as_uuid=True),
|
|
23
|
+
sa.ForeignKey("data_output_configurations.id", ondelete="CASCADE"),
|
|
24
|
+
primary_key=True,
|
|
25
|
+
),
|
|
26
|
+
sa.Column("catalog", sa.String(), nullable=True),
|
|
27
|
+
sa.Column("schema", sa.String(), nullable=True),
|
|
28
|
+
sa.Column("bucket_identifier", sa.String(), nullable=True),
|
|
29
|
+
sa.Column("catalog_path", sa.String(), nullable=True),
|
|
30
|
+
sa.Column("table", sa.String(), nullable=True),
|
|
31
|
+
sa.Column("table_path", sa.String(), nullable=True),
|
|
32
|
+
sa.Column("access_granularity", sa.String(), nullable=True),
|
|
33
|
+
sa.Column("created_on", sa.DateTime(timezone=False), server_default=utcnow()),
|
|
34
|
+
sa.Column("updated_on", sa.DateTime(timezone=False), onupdate=utcnow()),
|
|
35
|
+
sa.Column("deleted_at", sa.DateTime(timezone=False), nullable=True),
|
|
36
|
+
if_not_exists=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def downgrade() -> None:
|
|
41
|
+
op.drop_table(TABLE, if_exists=True)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1em" height="1em" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z" fill="#090B0B"/>
|
|
3
|
+
</svg>
|