From f2cd906f87fb319bf352ad69a73366160028e4db Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 08:13:10 +0900 Subject: [PATCH 01/56] APITokenSelector widget --- pyatlan/pkg/__init__.py | 0 pyatlan/pkg/widgets.py | 81 ++++++++++++++++++++++++++++ tests/unit/pkg/__init__.py | 0 tests/unit/pkg/test_widgets.py | 96 ++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 pyatlan/pkg/__init__.py create mode 100644 pyatlan/pkg/widgets.py create mode 100644 tests/unit/pkg/__init__.py create mode 100644 tests/unit/pkg/test_widgets.py diff --git a/pyatlan/pkg/__init__.py b/pyatlan/pkg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py new file mode 100644 index 000000000..421f6c031 --- /dev/null +++ b/pyatlan/pkg/widgets.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Atlan Pte. Ltd. +import abc +from dataclasses import dataclass + +# from dataclasses import dataclass +from typing import Optional + +from pydantic import StrictBool, StrictInt, StrictStr, validate_arguments + +# from pydantic.dataclasses import dataclass + + +@dataclass +class Widget(abc.ABC): + widget: str + label: str + hidden: bool = False + help: str = "" + placeholder: str = "" + grid: int = 8 + + +@dataclass +class UIElement(abc.ABC): + type: str + required: bool + ui: Optional[Widget] + + +@dataclass +class UIElementWithEnum(UIElement): + default: Optional[str] + enum: list[str] + enum_names: list[str] + + def __init__( + self, + type: str, + required: bool, + possible_values: dict[str, str], + ui: Optional[Widget] = None, + ): + super().__init__(type=type, required=required, ui=ui) + self.enum = list(possible_values.keys()) + self.enum_names = list(possible_values.values()) + + +@dataclass +class APITokenSelectorWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help: str = "", + grid: int = 4, + ): + super().__init__( + widget="apiTokenSelect", + label=label, + hidden=hidden, + help=help, + grid=grid, + ) + + +@dataclass +class APITokenSelector(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + grid: StrictInt = 4, + ): + widget = APITokenSelectorWidget( + label=label, hidden=hidden, help=help, grid=grid + ) + super().__init__(type="string", required=required, ui=widget) diff --git a/tests/unit/pkg/__init__.py b/tests/unit/pkg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py new file mode 100644 index 000000000..9003827f7 --- /dev/null +++ b/tests/unit/pkg/test_widgets.py @@ -0,0 +1,96 @@ +import pytest +from pydantic import ValidationError + +from pyatlan.pkg.widgets import APITokenSelector + +LABEL: str = "Some label" +HELP: str = "Some help text" +IS_REQUIRED = True +IS_NOT_REQUIRED = False +IS_HIDDEN = True +IS_NOT_HIDDEN = False + + +class TestAPITokenSelector: + def test_constructor_with_defaults(self): + sut = APITokenSelector(LABEL) + assert sut.type == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert ui.widget == "apiTokenSelect" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.grid == 4 + + def test_constructor_with_overrides(self): + sut = APITokenSelector( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + grid=(grid := 1), + ) + assert sut.type == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert ui.widget == "apiTokenSelect" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help, grid, msg", + [ + ( + 1, + True, + True, + HELP, + 1, + r"1 validation error for Init\nlabel\n str type expected", + ), + ( + LABEL, + 1, + True, + HELP, + 1, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 1, + HELP, + 1, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 1, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1.0, + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help, grid, msg): + with pytest.raises(ValidationError, match=msg): + APITokenSelector( + label=label, required=required, hidden=hidden, help=help, grid=grid + ) From d4e689ec3b33792559b03256bf51bcb42c9dff4a Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 08:16:13 +0900 Subject: [PATCH 02/56] Update unit test --- tests/unit/pkg/test_widgets.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 9003827f7..2624e2baf 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -47,6 +47,14 @@ def test_constructor_with_overrides(self): @pytest.mark.parametrize( "label, required, hidden, help, grid, msg", [ + ( + None, + True, + True, + HELP, + 1, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), ( 1, True, From 95493312d1a46d22ee90e7b2e0cb6fde62c23671 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 08:28:51 +0900 Subject: [PATCH 03/56] Add BooleanInput --- pyatlan/pkg/widgets.py | 35 ++++++++++++ tests/unit/pkg/test_widgets.py | 97 +++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 421f6c031..da29eb0ff 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -79,3 +79,38 @@ def __init__( label=label, hidden=hidden, help=help, grid=grid ) super().__init__(type="string", required=required, ui=widget) + + +@dataclass +class BooleanInputWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help: str = "", + placeholder: str = "", + grid: int = 8, + ): + super().__init__( + widget="boolean", + label=label, + hidden=hidden, + help=help, + placeholder=placeholder, + grid=grid, + ) + + +@dataclass +class BooleanInput(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + grid: StrictInt = 8, + ): + widget = BooleanInputWidget(label=label, hidden=hidden, help=help, grid=grid) + super().__init__(type="boolean", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 2624e2baf..b90a2365c 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -1,7 +1,7 @@ import pytest from pydantic import ValidationError -from pyatlan.pkg.widgets import APITokenSelector +from pyatlan.pkg.widgets import APITokenSelector, BooleanInput, BooleanInputWidget LABEL: str = "Some label" HELP: str = "Some help text" @@ -102,3 +102,98 @@ def test_validation(self, label, required, hidden, help, grid, msg): APITokenSelector( label=label, required=required, hidden=hidden, help=help, grid=grid ) + + +class TestBooleanInput: + def test_constructor_with_defaults(self): + sut = BooleanInput(label=LABEL) + assert sut.type == "boolean" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, BooleanInputWidget) + assert ui.widget == "boolean" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = BooleanInput( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + grid=(grid := 3), + ) + assert sut.type == "boolean" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, BooleanInputWidget) + assert ui.widget == "boolean" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help, grid, msg", + [ + ( + None, + True, + True, + HELP, + 1, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + 1, + True, + True, + HELP, + 1, + r"1 validation error for Init\nlabel\n str type expected", + ), + ( + LABEL, + 1, + True, + HELP, + 1, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 1, + HELP, + 1, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 1, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1.0, + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help, grid, msg): + with pytest.raises(ValidationError, match=msg): + BooleanInput( + label=label, required=required, hidden=hidden, help=help, grid=grid + ) From 1f7dfc3e04b0f3a55ec9ec0927aed35b58a7c92c Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 08:44:30 +0900 Subject: [PATCH 04/56] Add ConnectionCreator --- pyatlan/pkg/widgets.py | 31 ++++++++++ tests/unit/pkg/test_widgets.py | 110 ++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index da29eb0ff..7b1a2c8aa 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -114,3 +114,34 @@ def __init__( ): widget = BooleanInputWidget(label=label, hidden=hidden, help=help, grid=grid) super().__init__(type="boolean", required=required, ui=widget) + + +@dataclass +class ConnectionCreatorWidget(Widget): + def __init__( + self, label: str, hidden: bool = False, help: str = "", placeholder: str = "" + ): + super().__init__( + widget="connection", + label=label, + hidden=hidden, + help=help, + placeholder=placeholder, + ) + + +@dataclass +class ConnectionCreator(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + placeholder: StrictStr = "", + ): + widget = ConnectionCreatorWidget( + label=label, hidden=hidden, help=help, placeholder=placeholder + ) + super().__init__(type="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index b90a2365c..407112adc 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -1,10 +1,18 @@ import pytest from pydantic import ValidationError -from pyatlan.pkg.widgets import APITokenSelector, BooleanInput, BooleanInputWidget +from pyatlan.pkg.widgets import ( + APITokenSelector, + APITokenSelectorWidget, + BooleanInput, + BooleanInputWidget, + ConnectionCreator, + ConnectionCreatorWidget, +) LABEL: str = "Some label" HELP: str = "Some help text" +PLACE_HOLDER: str = "something goes here" IS_REQUIRED = True IS_NOT_REQUIRED = False IS_HIDDEN = True @@ -38,6 +46,7 @@ def test_constructor_with_overrides(self): ui = sut.ui assert ui + assert isinstance(ui, APITokenSelectorWidget) assert ui.widget == "apiTokenSelect" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN @@ -197,3 +206,102 @@ def test_validation(self, label, required, hidden, help, grid, msg): BooleanInput( label=label, required=required, hidden=hidden, help=help, grid=grid ) + + +class TestConnectionCreator: + def test_constructor_with_defaults(self): + sut = ConnectionCreator(label=LABEL) + assert sut.type == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, ConnectionCreatorWidget) + assert ui.widget == "connection" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.placeholder == "" + + def test_constructor_with_overrides(self): + sut = ConnectionCreator( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + placeholder=PLACE_HOLDER, + ) + assert sut.type == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, ConnectionCreatorWidget) + assert ui.widget == "connection" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.placeholder == PLACE_HOLDER + + @pytest.mark.parametrize( + "label, required, hidden, help, placeholder, msg", + [ + ( + None, + True, + True, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + 1, + True, + True, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nlabel\n str type expected", + ), + ( + LABEL, + 1, + True, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 1, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + PLACE_HOLDER, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1.0, + r"1 validation error for Init\nplaceholder\n str type expected", + ), + ], + ) + def test_validation(self, label, required, hidden, help, placeholder, msg): + with pytest.raises(ValidationError, match=msg): + ConnectionCreator( + label=label, + required=required, + hidden=hidden, + help=help, + placeholder=placeholder, + ) From 7fa305a12e6ae9032b8db41abada3b693122083c Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 09:05:19 +0900 Subject: [PATCH 05/56] Add ConnectionSelector --- pyatlan/pkg/widgets.py | 51 ++++++++++++ tests/unit/pkg/test_widgets.py | 143 +++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 7b1a2c8aa..64003665d 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -145,3 +145,54 @@ def __init__( label=label, hidden=hidden, help=help, placeholder=placeholder ) super().__init__(type="string", required=required, ui=widget) + + +@dataclass +class ConnectionSelectorWidget(Widget): + start: int = 1 + + def __init__( + self, + label: str, + hidden: bool = False, + help: str = "", + placeholder: str = "", + grid: int = 4, + start: int = 1, + ): + super().__init__( + widget="connectionSelector", + label=label, + hidden=hidden, + help=help, + placeholder=placeholder, + grid=grid, + ) + self.start = start + + +@dataclass +class ConnectionSelector(UIElement): + start: StrictInt = 1 + + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + placeholder: StrictStr = "", + grid: StrictInt = 4, + start: StrictInt = 1, + ): + widget = ConnectionSelectorWidget( + label=label, + hidden=hidden, + help=help, + placeholder=placeholder, + grid=grid, + start=start, + ) + super().__init__(type="string", required=required, ui=widget) + self.start = start diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 407112adc..98cf3c066 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -8,6 +8,8 @@ BooleanInputWidget, ConnectionCreator, ConnectionCreatorWidget, + ConnectionSelector, + ConnectionSelectorWidget, ) LABEL: str = "Some label" @@ -305,3 +307,144 @@ def test_validation(self, label, required, hidden, help, placeholder, msg): help=help, placeholder=placeholder, ) + + +class TestConnectionSelector: + def test_constructor_with_defaults(self): + sut = ConnectionSelector(label=LABEL) + assert sut.type == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, ConnectionSelectorWidget) + assert ui.widget == "connectionSelector" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.placeholder == "" + assert ui.grid == 4 + assert ui.start == 1 + + def test_constructor_with_overrides(self): + sut = ConnectionSelector( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + placeholder=PLACE_HOLDER, + grid=(grid := 2), + start=(start := 10), + ) + assert sut.type == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, ConnectionSelectorWidget) + assert ui.widget == "connectionSelector" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.placeholder == PLACE_HOLDER + assert ui.grid == grid + assert ui.start == start + + @pytest.mark.parametrize( + "label, required, hidden, help, placeholder, grid, start, msg", + [ + ( + None, + True, + True, + HELP, + PLACE_HOLDER, + 1, + 2, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + 1, + True, + True, + HELP, + PLACE_HOLDER, + 1, + 2, + r"1 validation error for Init\nlabel\n str type expected", + ), + ( + LABEL, + 1, + True, + HELP, + PLACE_HOLDER, + 1, + 2, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 1, + HELP, + PLACE_HOLDER, + 1, + 2, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + PLACE_HOLDER, + 1, + 2, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1.0, + 1, + 2, + r"1 validation error for Init\nplaceholder\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + PLACE_HOLDER, + 1.0, + 2, + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ( + LABEL, + True, + True, + HELP, + PLACE_HOLDER, + 1, + 2.0, + r"1 validation error for Init\nstart\n value is not a valid integer", + ), + ], + ) + def test_validation( + self, label, required, hidden, help, placeholder, grid, start, msg + ): + with pytest.raises(ValidationError, match=msg): + ConnectionSelector( + label=label, + required=required, + hidden=hidden, + help=help, + placeholder=placeholder, + grid=grid, + start=start, + ) From 0050d542fc3a7b508d9d2168b04bdffc0a268c5a Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 11:12:35 +0900 Subject: [PATCH 06/56] Add ConnectorTypeSelector --- pyatlan/pkg/widgets.py | 44 ++++++++++++ tests/unit/pkg/test_widgets.py | 120 +++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 64003665d..30ea44c86 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -196,3 +196,47 @@ def __init__( ) super().__init__(type="string", required=required, ui=widget) self.start = start + + +@dataclass +class ConnectorTypeSelectorWidget(Widget): + start: int = 1 + + def __init__( + self, + label: str, + hidden: bool = False, + help: str = "", + grid: int = 4, + start: int = 1, + ): + super().__init__( + widget="sourceConnectionSelector", + label=label, + hidden=hidden, + help=help, + grid=grid, + ) + self.start = start + + +@dataclass +class ConnectorTypeSelector(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + grid: StrictInt = 4, + start: StrictInt = 1, + ): + widget = ConnectorTypeSelectorWidget( + label=label, + hidden=hidden, + help=help, + grid=grid, + start=start, + ) + super().__init__(type="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 98cf3c066..47998d0b7 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -10,6 +10,8 @@ ConnectionCreatorWidget, ConnectionSelector, ConnectionSelectorWidget, + ConnectorTypeSelector, + ConnectorTypeSelectorWidget, ) LABEL: str = "Some label" @@ -448,3 +450,121 @@ def test_validation( grid=grid, start=start, ) + + +class TestConnectorTypeSelector: + def test_constructor_with_defaults(self): + sut = ConnectorTypeSelector(label=LABEL) + assert sut.type == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, ConnectorTypeSelectorWidget) + assert ui.widget == "sourceConnectionSelector" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.grid == 4 + assert ui.start == 1 + + def test_constructor_with_overrides(self): + sut = ConnectorTypeSelector( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + grid=(grid := 2), + start=(start := 10), + ) + assert sut.type == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, ConnectorTypeSelectorWidget) + assert ui.widget == "sourceConnectionSelector" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.grid == grid + assert ui.start == start + + @pytest.mark.parametrize( + "label, required, hidden, help, grid, start, msg", + [ + ( + None, + True, + True, + HELP, + 1, + 2, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + 1, + True, + True, + HELP, + 1, + 2, + r"1 validation error for Init\nlabel\n str type expected", + ), + ( + LABEL, + 1, + True, + HELP, + 1, + 2, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 1, + HELP, + 1, + 2, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 1, + 2, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1.0, + 2, + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ( + LABEL, + True, + True, + HELP, + 1, + 2.0, + r"1 validation error for Init\nstart\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help, grid, start, msg): + with pytest.raises(ValidationError, match=msg): + ConnectorTypeSelector( + label=label, + required=required, + hidden=hidden, + help=help, + grid=grid, + start=start, + ) From 077b507eefd4c424aed04032f769d1afe79a577c Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 12:09:06 +0900 Subject: [PATCH 07/56] Add DateInput --- pyatlan/pkg/widgets.py | 55 ++++++++++ tests/unit/pkg/test_widgets.py | 179 +++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 30ea44c86..ff223ef98 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -240,3 +240,58 @@ def __init__( start=start, ) super().__init__(type="string", required=required, ui=widget) + + +@dataclass +class DateInputWidget(Widget): + min: int = (-14,) + max: int = (0,) + default: int = (0,) + start: int = 1 + + def __init__( + self, + label: str, + hidden: bool = False, + help: str = "", + min: int = -14, + max: int = 0, + default: int = 0, + start: int = 1, + grid: int = 4, + ): + super().__init__( + widget="date", label=label, hidden=hidden, help=help, grid=grid + ) + self.start = start + self.max = max + self.min = min + self.default = default + + +@dataclass +class DateInput(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + min: StrictInt = -14, + max: StrictInt = 0, + default: StrictInt = 0, + start: StrictInt = 1, + grid: StrictInt = 8, + ): + widget = DateInputWidget( + label=label, + hidden=hidden, + help=help, + min=min, + max=max, + default=default, + start=start, + grid=grid, + ) + super().__init__(type="number", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 47998d0b7..6e9125429 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -12,6 +12,8 @@ ConnectionSelectorWidget, ConnectorTypeSelector, ConnectorTypeSelectorWidget, + DateInput, + DateInputWidget, ) LABEL: str = "Some label" @@ -568,3 +570,180 @@ def test_validation(self, label, required, hidden, help, grid, start, msg): grid=grid, start=start, ) + + +class TestDateInput: + def test_constructor_with_defaults(self): + sut = DateInput(label=LABEL) + assert sut.type == "number" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, DateInputWidget) + assert ui.widget == "date" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.min == -14 + assert ui.max == 0 + assert ui.default == 0 + assert ui.start == 1 + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = DateInput( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + min=(min := -2), + max=(max := 3), + default=(default := 1), + start=(start := 10), + grid=(grid := 2), + ) + assert sut.type == "number" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, DateInputWidget) + assert ui.widget == "date" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.min == min + assert ui.max == max + assert ui.default == default + assert ui.start == start + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help, min, max, default, start, grid, msg", + [ + ( + None, + True, + True, + HELP, + -5, + 3, + 1, + 0, + 4, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 1, + True, + HELP, + -5, + 3, + 1, + 0, + 4, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 1, + HELP, + -5, + 3, + 1, + 0, + 4, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + -5, + 3, + 1, + 0, + 4, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "a", + 3, + 1, + 0, + 4, + r"1 validation error for Init\nmin\n value is not a valid integer", + ), + ( + LABEL, + True, + True, + HELP, + -5, + "a", + 1, + 0, + 4, + r"1 validation error for Init\nmax\n value is not a valid integer", + ), + ( + LABEL, + True, + True, + HELP, + -5, + 3, + "a", + 0, + 4, + r"1 validation error for Init\ndefault\n value is not a valid integer", + ), + ( + LABEL, + True, + True, + HELP, + -5, + 3, + 1, + "a", + 4, + r"1 validation error for Init\nstart\n value is not a valid integer", + ), + ( + LABEL, + True, + True, + HELP, + -5, + 3, + 1, + 0, + "4", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation( + self, label, required, hidden, help, min, max, default, start, grid, msg + ): + with pytest.raises(ValidationError, match=msg): + DateInput( + label=label, + required=required, + hidden=hidden, + help=help, + min=min, + max=max, + default=default, + start=start, + grid=grid, + ) From 1c29b554437bb4abee4b81abd9339c460822354b Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 12:11:04 +0900 Subject: [PATCH 08/56] Fix mypy violations --- pyatlan/pkg/widgets.py | 6 +++--- pyatlan/samples/search/and_star_assets.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index ff223ef98..a699eba23 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -244,9 +244,9 @@ def __init__( @dataclass class DateInputWidget(Widget): - min: int = (-14,) - max: int = (0,) - default: int = (0,) + min: int = -14 + max: int = 0 + default: int = 0 start: int = 1 def __init__( diff --git a/pyatlan/samples/search/and_star_assets.py b/pyatlan/samples/search/and_star_assets.py index ec530a9b5..4138cb3d5 100644 --- a/pyatlan/samples/search/and_star_assets.py +++ b/pyatlan/samples/search/and_star_assets.py @@ -27,7 +27,7 @@ def find_assets() -> IndexSearchResults: FluentSearch() .where(FluentSearch.active_assets()) .where(FluentSearch.asset_type(AtlasGlossaryTerm)) - .where(AtlasGlossaryTerm.ANCHOR.eq(glossary.qualified_name)) + .where(AtlasGlossaryTerm.ANCHOR.eq(glossary.qualified_name or "")) .page_size(100) .include_on_results(Asset.STARRED_DETAILS_LIST) .include_on_results(Asset.STARRED_BY) From 39af1bd873ab0a45dd2b2818142d1613a0a4afd3 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 13:42:31 +0900 Subject: [PATCH 09/56] Add DropDown --- pyatlan/pkg/widgets.py | 50 ++++++++++++ tests/unit/pkg/test_widgets.py | 134 +++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index a699eba23..4b4c33377 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -295,3 +295,53 @@ def __init__( grid=grid, ) super().__init__(type="number", required=required, ui=widget) + + +@dataclass +class DropDownWidget(Widget): + mode: str = "" + + def __init__( + self, + label: str, + mode: str, + hidden: bool = False, + help: str = "", + grid: int = 8, + ): + super().__init__( + widget="select", + label=label, + hidden=hidden, + help=help, + grid=grid, + ) + self.mode = mode + + +@dataclass +class DropDown(UIElementWithEnum): + possible_values: dict[str, str] + + @validate_arguments() + def __init__( + self, + label: StrictStr, + possible_values: dict[str, str], + required: StrictBool = False, + hidden: StrictBool = False, + help: StrictStr = "", + multi_select: StrictBool = False, + grid: StrictInt = 8, + ): + widget = DropDownWidget( + label=label, + mode="multiple" if multi_select else "", + hidden=hidden, + help=help, + grid=grid, + ) + super().__init__( + type="string", required=required, possible_values=possible_values, ui=widget + ) + self.possible_values = possible_values diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 6e9125429..e79d1e058 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -14,6 +14,8 @@ ConnectorTypeSelectorWidget, DateInput, DateInputWidget, + DropDown, + DropDownWidget, ) LABEL: str = "Some label" @@ -23,6 +25,7 @@ IS_NOT_REQUIRED = False IS_HIDDEN = True IS_NOT_HIDDEN = False +POSSIBLE_VALUES = {"name": "Dave"} class TestAPITokenSelector: @@ -747,3 +750,134 @@ def test_validation( start=start, grid=grid, ) + + +class TestDropDown: + def test_constructor_with_defaults(self): + sut = DropDown(label=LABEL, possible_values=POSSIBLE_VALUES) + assert sut.type == "string" + assert sut.required == IS_NOT_REQUIRED + assert sut.possible_values == POSSIBLE_VALUES + + ui = sut.ui + assert ui + assert isinstance(ui, DropDownWidget) + assert ui.widget == "select" + assert ui.label == LABEL + assert ui.mode == "" + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = DropDown( + label=LABEL, + possible_values=POSSIBLE_VALUES, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help=HELP, + multi_select=True, + grid=(grid := 2), + ) + assert sut.type == "string" + assert sut.required == IS_REQUIRED + assert sut.possible_values == POSSIBLE_VALUES + + ui = sut.ui + assert ui + assert isinstance(ui, DropDownWidget) + assert ui.widget == "select" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help == HELP + assert ui.mode == "multiple" + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, possible_values, required, hidden, help, multi_select, grid, msg", + [ + ( + None, + POSSIBLE_VALUES, + True, + True, + HELP, + False, + 4, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + None, + True, + True, + HELP, + False, + 4, + r"1 validation error for Init\npossible_values\n none is not an allowed value", + ), + ( + LABEL, + POSSIBLE_VALUES, + 1, + True, + HELP, + False, + 4, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + POSSIBLE_VALUES, + True, + 1, + HELP, + False, + 4, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + POSSIBLE_VALUES, + True, + True, + 1, + False, + 4, + r"1 validation error for Init\nhelp\n str type expected", + ), + ( + LABEL, + POSSIBLE_VALUES, + True, + True, + HELP, + 1, + 4, + r"1 validation error for Init\nmulti_select\n value is not a valid boolean", + ), + ( + LABEL, + POSSIBLE_VALUES, + True, + True, + HELP, + False, + "1", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation( + self, label, possible_values, required, hidden, help, multi_select, grid, msg + ): + with pytest.raises(ValidationError, match=msg): + DropDown( + label=label, + possible_values=possible_values, + required=required, + hidden=hidden, + help=help, + multi_select=multi_select, + grid=grid, + ) From 5950009c79f1832242340779be2c1bd01a9766a4 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 14:26:54 +0900 Subject: [PATCH 10/56] Add FileUploader --- pyatlan/pkg/widgets.py | 147 +++++++++++------- tests/unit/pkg/test_widgets.py | 262 +++++++++++++++++++++++---------- 2 files changed, 285 insertions(+), 124 deletions(-) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 4b4c33377..0ba22d0a0 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2023 Atlan Pte. Ltd. import abc -from dataclasses import dataclass +from dataclasses import dataclass, field # from dataclasses import dataclass from typing import Optional @@ -16,14 +16,14 @@ class Widget(abc.ABC): widget: str label: str hidden: bool = False - help: str = "" + help_: str = "" placeholder: str = "" grid: int = 8 @dataclass class UIElement(abc.ABC): - type: str + type_: str required: bool ui: Optional[Widget] @@ -36,12 +36,12 @@ class UIElementWithEnum(UIElement): def __init__( self, - type: str, + type_: str, required: bool, possible_values: dict[str, str], ui: Optional[Widget] = None, ): - super().__init__(type=type, required=required, ui=ui) + super().__init__(type_=type_, required=required, ui=ui) self.enum = list(possible_values.keys()) self.enum_names = list(possible_values.values()) @@ -52,14 +52,14 @@ def __init__( self, label: str, hidden: bool = False, - help: str = "", + help_: str = "", grid: int = 4, ): super().__init__( widget="apiTokenSelect", label=label, hidden=hidden, - help=help, + help_=help_, grid=grid, ) @@ -72,13 +72,13 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", + help_: StrictStr = "", grid: StrictInt = 4, ): widget = APITokenSelectorWidget( - label=label, hidden=hidden, help=help, grid=grid + label=label, hidden=hidden, help_=help_, grid=grid ) - super().__init__(type="string", required=required, ui=widget) + super().__init__(type_="string", required=required, ui=widget) @dataclass @@ -87,7 +87,7 @@ def __init__( self, label: str, hidden: bool = False, - help: str = "", + help_: str = "", placeholder: str = "", grid: int = 8, ): @@ -95,7 +95,7 @@ def __init__( widget="boolean", label=label, hidden=hidden, - help=help, + help_=help_, placeholder=placeholder, grid=grid, ) @@ -109,23 +109,23 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", + help_: StrictStr = "", grid: StrictInt = 8, ): - widget = BooleanInputWidget(label=label, hidden=hidden, help=help, grid=grid) - super().__init__(type="boolean", required=required, ui=widget) + widget = BooleanInputWidget(label=label, hidden=hidden, help_=help_, grid=grid) + super().__init__(type_="boolean", required=required, ui=widget) @dataclass class ConnectionCreatorWidget(Widget): def __init__( - self, label: str, hidden: bool = False, help: str = "", placeholder: str = "" + self, label: str, hidden: bool = False, help_: str = "", placeholder: str = "" ): super().__init__( widget="connection", label=label, hidden=hidden, - help=help, + help_=help_, placeholder=placeholder, ) @@ -138,13 +138,13 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", + help_: StrictStr = "", placeholder: StrictStr = "", ): widget = ConnectionCreatorWidget( - label=label, hidden=hidden, help=help, placeholder=placeholder + label=label, hidden=hidden, help_=help_, placeholder=placeholder ) - super().__init__(type="string", required=required, ui=widget) + super().__init__(type_="string", required=required, ui=widget) @dataclass @@ -155,7 +155,7 @@ def __init__( self, label: str, hidden: bool = False, - help: str = "", + help_: str = "", placeholder: str = "", grid: int = 4, start: int = 1, @@ -164,7 +164,7 @@ def __init__( widget="connectionSelector", label=label, hidden=hidden, - help=help, + help_=help_, placeholder=placeholder, grid=grid, ) @@ -181,7 +181,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", + help_: StrictStr = "", placeholder: StrictStr = "", grid: StrictInt = 4, start: StrictInt = 1, @@ -189,12 +189,12 @@ def __init__( widget = ConnectionSelectorWidget( label=label, hidden=hidden, - help=help, + help_=help_, placeholder=placeholder, grid=grid, start=start, ) - super().__init__(type="string", required=required, ui=widget) + super().__init__(type_="string", required=required, ui=widget) self.start = start @@ -206,7 +206,7 @@ def __init__( self, label: str, hidden: bool = False, - help: str = "", + help_: str = "", grid: int = 4, start: int = 1, ): @@ -214,7 +214,7 @@ def __init__( widget="sourceConnectionSelector", label=label, hidden=hidden, - help=help, + help_=help_, grid=grid, ) self.start = start @@ -228,24 +228,24 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", + help_: StrictStr = "", grid: StrictInt = 4, start: StrictInt = 1, ): widget = ConnectorTypeSelectorWidget( label=label, hidden=hidden, - help=help, + help_=help_, grid=grid, start=start, ) - super().__init__(type="string", required=required, ui=widget) + super().__init__(type_="string", required=required, ui=widget) @dataclass class DateInputWidget(Widget): - min: int = -14 - max: int = 0 + min_: int = -14 + max_: int = 0 default: int = 0 start: int = 1 @@ -253,19 +253,19 @@ def __init__( self, label: str, hidden: bool = False, - help: str = "", - min: int = -14, - max: int = 0, + help_: str = "", + min_: int = -14, + max_: int = 0, default: int = 0, start: int = 1, grid: int = 4, ): super().__init__( - widget="date", label=label, hidden=hidden, help=help, grid=grid + widget="date", label=label, hidden=hidden, help_=help_, grid=grid ) self.start = start - self.max = max - self.min = min + self.max_ = max_ + self.min_ = min_ self.default = default @@ -277,9 +277,9 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", - min: StrictInt = -14, - max: StrictInt = 0, + help_: StrictStr = "", + min_: StrictInt = -14, + max_: StrictInt = 0, default: StrictInt = 0, start: StrictInt = 1, grid: StrictInt = 8, @@ -287,14 +287,14 @@ def __init__( widget = DateInputWidget( label=label, hidden=hidden, - help=help, - min=min, - max=max, + help_=help_, + min_=min_, + max_=max_, default=default, start=start, grid=grid, ) - super().__init__(type="number", required=required, ui=widget) + super().__init__(type_="number", required=required, ui=widget) @dataclass @@ -306,14 +306,14 @@ def __init__( label: str, mode: str, hidden: bool = False, - help: str = "", + help_: str = "", grid: int = 8, ): super().__init__( widget="select", label=label, hidden=hidden, - help=help, + help_=help_, grid=grid, ) self.mode = mode @@ -330,7 +330,7 @@ def __init__( possible_values: dict[str, str], required: StrictBool = False, hidden: StrictBool = False, - help: StrictStr = "", + help_: StrictStr = "", multi_select: StrictBool = False, grid: StrictInt = 8, ): @@ -338,10 +338,57 @@ def __init__( label=label, mode="multiple" if multi_select else "", hidden=hidden, - help=help, + help_=help_, grid=grid, ) super().__init__( - type="string", required=required, possible_values=possible_values, ui=widget + type_="string", + required=required, + possible_values=possible_values, + ui=widget, ) self.possible_values = possible_values + + +@dataclass +class FileUploaderWidget(Widget): + file_types: list[str] = field(default_factory=list) + + def __init__( + self, + label: str, + file_types: list[str], + hidden: bool = False, + help_: str = "", + placeholder: str = "", + ): + super().__init__( + widget="fileUpload", + label=label, + hidden=hidden, + help_=help_, + placeholder=placeholder, + ) + self.file_types = file_types + + +@dataclass +class FileUploader(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + file_types: list[str], + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + placeholder: StrictStr = "", + ): + widget = FileUploaderWidget( + label=label, + file_types=file_types, + hidden=hidden, + help_=help_, + placeholder=placeholder, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index e79d1e058..d8f0a6a3c 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -16,22 +16,25 @@ DateInputWidget, DropDown, DropDownWidget, + FileUploader, + FileUploaderWidget, ) LABEL: str = "Some label" -HELP: str = "Some help text" +HELP: str = "Some help_ text" PLACE_HOLDER: str = "something goes here" IS_REQUIRED = True IS_NOT_REQUIRED = False IS_HIDDEN = True IS_NOT_HIDDEN = False POSSIBLE_VALUES = {"name": "Dave"} +FILE_TYPES = ["txt"] class TestAPITokenSelector: def test_constructor_with_defaults(self): sut = APITokenSelector(LABEL) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_NOT_REQUIRED ui = sut.ui @@ -39,7 +42,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "apiTokenSelect" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" + assert ui.help_ == "" assert ui.grid == 4 def test_constructor_with_overrides(self): @@ -47,10 +50,10 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, + help_=HELP, grid=(grid := 1), ) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_REQUIRED ui = sut.ui @@ -59,11 +62,11 @@ def test_constructor_with_overrides(self): assert ui.widget == "apiTokenSelect" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP + assert ui.help_ == HELP assert ui.grid == grid @pytest.mark.parametrize( - "label, required, hidden, help, grid, msg", + "label, required, hidden, help_, grid, msg", [ ( None, @@ -103,7 +106,7 @@ def test_constructor_with_overrides(self): True, 1, 1, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -115,17 +118,17 @@ def test_constructor_with_overrides(self): ), ], ) - def test_validation(self, label, required, hidden, help, grid, msg): + def test_validation(self, label, required, hidden, help_, grid, msg): with pytest.raises(ValidationError, match=msg): APITokenSelector( - label=label, required=required, hidden=hidden, help=help, grid=grid + label=label, required=required, hidden=hidden, help_=help_, grid=grid ) class TestBooleanInput: def test_constructor_with_defaults(self): sut = BooleanInput(label=LABEL) - assert sut.type == "boolean" + assert sut.type_ == "boolean" assert sut.required == IS_NOT_REQUIRED ui = sut.ui @@ -134,7 +137,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "boolean" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" + assert ui.help_ == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -142,10 +145,10 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, + help_=HELP, grid=(grid := 3), ) - assert sut.type == "boolean" + assert sut.type_ == "boolean" assert sut.required == IS_REQUIRED ui = sut.ui @@ -154,11 +157,11 @@ def test_constructor_with_overrides(self): assert ui.widget == "boolean" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP + assert ui.help_ == HELP assert ui.grid == grid @pytest.mark.parametrize( - "label, required, hidden, help, grid, msg", + "label, required, hidden, help_, grid, msg", [ ( None, @@ -198,7 +201,7 @@ def test_constructor_with_overrides(self): True, 1, 1, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -210,17 +213,17 @@ def test_constructor_with_overrides(self): ), ], ) - def test_validation(self, label, required, hidden, help, grid, msg): + def test_validation(self, label, required, hidden, help_, grid, msg): with pytest.raises(ValidationError, match=msg): BooleanInput( - label=label, required=required, hidden=hidden, help=help, grid=grid + label=label, required=required, hidden=hidden, help_=help_, grid=grid ) class TestConnectionCreator: def test_constructor_with_defaults(self): sut = ConnectionCreator(label=LABEL) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_NOT_REQUIRED ui = sut.ui @@ -229,7 +232,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "connection" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" + assert ui.help_ == "" assert ui.placeholder == "" def test_constructor_with_overrides(self): @@ -237,10 +240,10 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, + help_=HELP, placeholder=PLACE_HOLDER, ) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_REQUIRED ui = sut.ui @@ -249,11 +252,11 @@ def test_constructor_with_overrides(self): assert ui.widget == "connection" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP + assert ui.help_ == HELP assert ui.placeholder == PLACE_HOLDER @pytest.mark.parametrize( - "label, required, hidden, help, placeholder, msg", + "label, required, hidden, help_, placeholder, msg", [ ( None, @@ -293,7 +296,7 @@ def test_constructor_with_overrides(self): True, 1, PLACE_HOLDER, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -305,13 +308,13 @@ def test_constructor_with_overrides(self): ), ], ) - def test_validation(self, label, required, hidden, help, placeholder, msg): + def test_validation(self, label, required, hidden, help_, placeholder, msg): with pytest.raises(ValidationError, match=msg): ConnectionCreator( label=label, required=required, hidden=hidden, - help=help, + help_=help_, placeholder=placeholder, ) @@ -319,7 +322,7 @@ def test_validation(self, label, required, hidden, help, placeholder, msg): class TestConnectionSelector: def test_constructor_with_defaults(self): sut = ConnectionSelector(label=LABEL) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_NOT_REQUIRED ui = sut.ui @@ -328,7 +331,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "connectionSelector" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" + assert ui.help_ == "" assert ui.placeholder == "" assert ui.grid == 4 assert ui.start == 1 @@ -338,12 +341,12 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, + help_=HELP, placeholder=PLACE_HOLDER, grid=(grid := 2), start=(start := 10), ) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_REQUIRED ui = sut.ui @@ -352,13 +355,13 @@ def test_constructor_with_overrides(self): assert ui.widget == "connectionSelector" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP + assert ui.help_ == HELP assert ui.placeholder == PLACE_HOLDER assert ui.grid == grid assert ui.start == start @pytest.mark.parametrize( - "label, required, hidden, help, placeholder, grid, start, msg", + "label, required, hidden, help_, placeholder, grid, start, msg", [ ( None, @@ -408,7 +411,7 @@ def test_constructor_with_overrides(self): PLACE_HOLDER, 1, 2, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -443,14 +446,14 @@ def test_constructor_with_overrides(self): ], ) def test_validation( - self, label, required, hidden, help, placeholder, grid, start, msg + self, label, required, hidden, help_, placeholder, grid, start, msg ): with pytest.raises(ValidationError, match=msg): ConnectionSelector( label=label, required=required, hidden=hidden, - help=help, + help_=help_, placeholder=placeholder, grid=grid, start=start, @@ -460,7 +463,7 @@ def test_validation( class TestConnectorTypeSelector: def test_constructor_with_defaults(self): sut = ConnectorTypeSelector(label=LABEL) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_NOT_REQUIRED ui = sut.ui @@ -469,7 +472,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "sourceConnectionSelector" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" + assert ui.help_ == "" assert ui.grid == 4 assert ui.start == 1 @@ -478,11 +481,11 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, + help_=HELP, grid=(grid := 2), start=(start := 10), ) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_REQUIRED ui = sut.ui @@ -491,12 +494,12 @@ def test_constructor_with_overrides(self): assert ui.widget == "sourceConnectionSelector" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP + assert ui.help_ == HELP assert ui.grid == grid assert ui.start == start @pytest.mark.parametrize( - "label, required, hidden, help, grid, start, msg", + "label, required, hidden, help_, grid, start, msg", [ ( None, @@ -541,7 +544,7 @@ def test_constructor_with_overrides(self): 1, 1, 2, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -563,13 +566,13 @@ def test_constructor_with_overrides(self): ), ], ) - def test_validation(self, label, required, hidden, help, grid, start, msg): + def test_validation(self, label, required, hidden, help_, grid, start, msg): with pytest.raises(ValidationError, match=msg): ConnectorTypeSelector( label=label, required=required, hidden=hidden, - help=help, + help_=help_, grid=grid, start=start, ) @@ -578,7 +581,7 @@ def test_validation(self, label, required, hidden, help, grid, start, msg): class TestDateInput: def test_constructor_with_defaults(self): sut = DateInput(label=LABEL) - assert sut.type == "number" + assert sut.type_ == "number" assert sut.required == IS_NOT_REQUIRED ui = sut.ui @@ -587,9 +590,9 @@ def test_constructor_with_defaults(self): assert ui.widget == "date" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" - assert ui.min == -14 - assert ui.max == 0 + assert ui.help_ == "" + assert ui.min_ == -14 + assert ui.max_ == 0 assert ui.default == 0 assert ui.start == 1 assert ui.grid == 8 @@ -599,14 +602,14 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, - min=(min := -2), - max=(max := 3), + help_=HELP, + min_=(min_ := -2), + max_=(max_ := 3), default=(default := 1), start=(start := 10), grid=(grid := 2), ) - assert sut.type == "number" + assert sut.type_ == "number" assert sut.required == IS_REQUIRED ui = sut.ui @@ -615,15 +618,15 @@ def test_constructor_with_overrides(self): assert ui.widget == "date" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP - assert ui.min == min - assert ui.max == max + assert ui.help_ == HELP + assert ui.min_ == min_ + assert ui.max_ == max_ assert ui.default == default assert ui.start == start assert ui.grid == grid @pytest.mark.parametrize( - "label, required, hidden, help, min, max, default, start, grid, msg", + "label, required, hidden, help_, min_, max_, default, start, grid, msg", [ ( None, @@ -671,7 +674,7 @@ def test_constructor_with_overrides(self): 1, 0, 4, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -683,7 +686,7 @@ def test_constructor_with_overrides(self): 1, 0, 4, - r"1 validation error for Init\nmin\n value is not a valid integer", + r"1 validation error for Init\nmin_\n value is not a valid integer", ), ( LABEL, @@ -695,7 +698,7 @@ def test_constructor_with_overrides(self): 1, 0, 4, - r"1 validation error for Init\nmax\n value is not a valid integer", + r"1 validation error for Init\nmax_\n value is not a valid integer", ), ( LABEL, @@ -736,16 +739,16 @@ def test_constructor_with_overrides(self): ], ) def test_validation( - self, label, required, hidden, help, min, max, default, start, grid, msg + self, label, required, hidden, help_, min_, max_, default, start, grid, msg ): with pytest.raises(ValidationError, match=msg): DateInput( label=label, required=required, hidden=hidden, - help=help, - min=min, - max=max, + help_=help_, + min_=min_, + max_=max_, default=default, start=start, grid=grid, @@ -755,7 +758,7 @@ def test_validation( class TestDropDown: def test_constructor_with_defaults(self): sut = DropDown(label=LABEL, possible_values=POSSIBLE_VALUES) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_NOT_REQUIRED assert sut.possible_values == POSSIBLE_VALUES @@ -766,7 +769,7 @@ def test_constructor_with_defaults(self): assert ui.label == LABEL assert ui.mode == "" assert ui.hidden == IS_NOT_HIDDEN - assert ui.help == "" + assert ui.help_ == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -775,11 +778,11 @@ def test_constructor_with_overrides(self): possible_values=POSSIBLE_VALUES, required=IS_REQUIRED, hidden=IS_HIDDEN, - help=HELP, + help_=HELP, multi_select=True, grid=(grid := 2), ) - assert sut.type == "string" + assert sut.type_ == "string" assert sut.required == IS_REQUIRED assert sut.possible_values == POSSIBLE_VALUES @@ -789,12 +792,12 @@ def test_constructor_with_overrides(self): assert ui.widget == "select" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help == HELP + assert ui.help_ == HELP assert ui.mode == "multiple" assert ui.grid == grid @pytest.mark.parametrize( - "label, possible_values, required, hidden, help, multi_select, grid, msg", + "label, possible_values, required, hidden, help_, multi_select, grid, msg", [ ( None, @@ -844,7 +847,7 @@ def test_constructor_with_overrides(self): 1, False, 4, - r"1 validation error for Init\nhelp\n str type expected", + r"1 validation error for Init\nhelp_\n str type expected", ), ( LABEL, @@ -869,7 +872,7 @@ def test_constructor_with_overrides(self): ], ) def test_validation( - self, label, possible_values, required, hidden, help, multi_select, grid, msg + self, label, possible_values, required, hidden, help_, multi_select, grid, msg ): with pytest.raises(ValidationError, match=msg): DropDown( @@ -877,7 +880,118 @@ def test_validation( possible_values=possible_values, required=required, hidden=hidden, - help=help, + help_=help_, multi_select=multi_select, grid=grid, ) + + +class TestFileUploader: + def test_constructor_with_defaults(self): + sut = FileUploader(label=LABEL, file_types=FILE_TYPES) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, FileUploaderWidget) + assert ui.widget == "fileUpload" + assert ui.label == LABEL + assert ui.file_types == FILE_TYPES + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.placeholder == "" + + def test_constructor_with_overrides(self): + sut = FileUploader( + label=LABEL, + file_types=FILE_TYPES, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + placeholder=PLACE_HOLDER, + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, FileUploaderWidget) + assert ui.widget == "fileUpload" + assert ui.label == LABEL + assert ui.file_types == FILE_TYPES + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.placeholder == PLACE_HOLDER + + @pytest.mark.parametrize( + "label, file_types, required, hidden, help_, placeholder, msg", + [ + ( + None, + FILE_TYPES, + True, + True, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + None, + True, + True, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nfile_types\n none is not an allowed value", + ), + ( + LABEL, + FILE_TYPES, + 1, + True, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + FILE_TYPES, + True, + 0, + HELP, + PLACE_HOLDER, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + FILE_TYPES, + True, + True, + 1, + PLACE_HOLDER, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + FILE_TYPES, + True, + True, + HELP, + 1, + r"1 validation error for Init\nplaceholder\n str type expected", + ), + ], + ) + def test_validation( + self, label, file_types, required, hidden, help_, placeholder, msg + ): + with pytest.raises(ValidationError, match=msg): + FileUploader( + label=label, + file_types=file_types, + required=required, + hidden=hidden, + help_=help_, + placeholder=placeholder, + ) From d850ac6fda4fc27f7d768fa891a79a1c1d6e7cca Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 14:51:51 +0900 Subject: [PATCH 11/56] Add KeygenInput --- pyatlan/pkg/widgets.py | 34 ++++++++++++ tests/unit/pkg/test_widgets.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 0ba22d0a0..6404709a8 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -392,3 +392,37 @@ def __init__( placeholder=placeholder, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class KeygenInputWidget(Widget): + def __init__( + self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 + ): + super().__init__( + widget="keygen", + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + + +@dataclass +class KeygenInput(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + grid: StrictInt = 8, + ): + widget = KeygenInputWidget( + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index d8f0a6a3c..41fa9ec76 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -18,6 +18,8 @@ DropDownWidget, FileUploader, FileUploaderWidget, + KeygenInput, + KeygenInputWidget, ) LABEL: str = "Some label" @@ -995,3 +997,96 @@ def test_validation( help_=help_, placeholder=placeholder, ) + + +class TestKeygenInput: + def test_constructor_with_defaults(self): + sut = KeygenInput( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, KeygenInputWidget) + assert ui.widget == "keygen" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = KeygenInput( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, KeygenInputWidget) + assert ui.widget == "keygen" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, grid, msg", + [ + ( + None, + True, + True, + HELP, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, grid, msg): + with pytest.raises(ValidationError, match=msg): + KeygenInput( + label=label, + required=required, + hidden=hidden, + help_=help_, + grid=grid, + ) From d2c37a09fde84d93b757f4c1a884bf01de28207b Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 14:57:17 +0900 Subject: [PATCH 12/56] Add MultipleGroups --- pyatlan/pkg/widgets.py | 34 ++++++++++++ tests/unit/pkg/test_widgets.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 6404709a8..899715e3f 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -426,3 +426,37 @@ def __init__( grid=grid, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class MultipleGroupsWidget(Widget): + def __init__( + self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 + ): + super().__init__( + widget="groupMultiple", + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + + +@dataclass +class MultipleGroups(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + grid: StrictInt = 8, + ): + widget = MultipleGroupsWidget( + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 41fa9ec76..83b25a05f 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -20,6 +20,8 @@ FileUploaderWidget, KeygenInput, KeygenInputWidget, + MultipleGroups, + MultipleGroupsWidget, ) LABEL: str = "Some label" @@ -1090,3 +1092,96 @@ def test_validation(self, label, required, hidden, help_, grid, msg): help_=help_, grid=grid, ) + + +class TestMultipleGroups: + def test_constructor_with_defaults(self): + sut = MultipleGroups( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, MultipleGroupsWidget) + assert ui.widget == "groupMultiple" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = MultipleGroups( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, MultipleGroupsWidget) + assert ui.widget == "groupMultiple" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, grid, msg", + [ + ( + None, + True, + True, + HELP, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, grid, msg): + with pytest.raises(ValidationError, match=msg): + MultipleGroups( + label=label, + required=required, + hidden=hidden, + help_=help_, + grid=grid, + ) From 23689d9a9f826b9700109d8a1e3b3086a23ad190 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:01:38 +0900 Subject: [PATCH 13/56] Add MultipleUsers --- pyatlan/pkg/widgets.py | 34 ++++++++++++ tests/unit/pkg/test_widgets.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 899715e3f..e7d5ae803 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -460,3 +460,37 @@ def __init__( grid=grid, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class MultipleUsersWidget(Widget): + def __init__( + self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 + ): + super().__init__( + widget="groupMultiple", + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + + +@dataclass +class MultipleUsers(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + grid: StrictInt = 8, + ): + widget = MultipleUsersWidget( + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 83b25a05f..0d5e5e8be 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -22,6 +22,8 @@ KeygenInputWidget, MultipleGroups, MultipleGroupsWidget, + MultipleUsers, + MultipleUsersWidget, ) LABEL: str = "Some label" @@ -1185,3 +1187,96 @@ def test_validation(self, label, required, hidden, help_, grid, msg): help_=help_, grid=grid, ) + + +class TestMultipleUsers: + def test_constructor_with_defaults(self): + sut = MultipleUsers( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, MultipleUsersWidget) + assert ui.widget == "groupMultiple" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = MultipleUsers( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, MultipleUsersWidget) + assert ui.widget == "groupMultiple" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, grid, msg", + [ + ( + None, + True, + True, + HELP, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, grid, msg): + with pytest.raises(ValidationError, match=msg): + MultipleUsers( + label=label, + required=required, + hidden=hidden, + help_=help_, + grid=grid, + ) From 4573fd20e9256e97a7dafaca127863e248d45227 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:16:04 +0900 Subject: [PATCH 14/56] Add NumericInput --- pyatlan/pkg/widgets.py | 42 ++++++++++++ tests/unit/pkg/test_widgets.py | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index e7d5ae803..9d54422d8 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -494,3 +494,45 @@ def __init__( grid=grid, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class NumericInputWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help_: str = "", + placeholder: str = "", + grid: int = 8, + ): + super().__init__( + widget="inputNumber", + label=label, + hidden=hidden, + help_=help_, + placeholder=placeholder, + grid=grid, + ) + + +@dataclass +class NumericInput(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + placeholder: StrictStr = "", + grid: StrictInt = 8, + ): + widget = NumericInputWidget( + label=label, + hidden=hidden, + help_=help_, + placeholder=placeholder, + grid=grid, + ) + super().__init__(type_="number", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 0d5e5e8be..5338a491b 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -24,6 +24,8 @@ MultipleGroupsWidget, MultipleUsers, MultipleUsersWidget, + NumericInput, + NumericInputWidget, ) LABEL: str = "Some label" @@ -1280,3 +1282,114 @@ def test_validation(self, label, required, hidden, help_, grid, msg): help_=help_, grid=grid, ) + + +class TestNumericInput: + def test_constructor_with_defaults(self): + sut = NumericInput( + label=LABEL, + ) + assert sut.type_ == "number" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, NumericInputWidget) + assert ui.widget == "inputNumber" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.placeholder == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = NumericInput( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + placeholder=PLACE_HOLDER, + grid=(grid := 3), + ) + assert sut.type_ == "number" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, NumericInputWidget) + assert ui.widget == "inputNumber" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.placeholder == PLACE_HOLDER + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, placeholder, grid, msg", + [ + ( + None, + True, + True, + HELP, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1, + 3, + r"1 validation error for Init\nplaceholder\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + PLACE_HOLDER, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, placeholder, grid, msg): + with pytest.raises(ValidationError, match=msg): + NumericInput( + label=label, + required=required, + hidden=hidden, + help_=help_, + placeholder=placeholder, + grid=grid, + ) From fca42020434c5133b583b9694a3201ef34cf30ef Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:24:41 +0900 Subject: [PATCH 15/56] Add PasswordInput --- pyatlan/pkg/widgets.py | 38 ++++++++++++++ tests/unit/pkg/test_widgets.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 9d54422d8..5b832ab0f 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -536,3 +536,41 @@ def __init__( grid=grid, ) super().__init__(type_="number", required=required, ui=widget) + + +@dataclass +class PasswordInputWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help_: str = "", + grid: int = 8, + ): + super().__init__( + widget="password", + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + + +@dataclass +class PasswordInput(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + grid: StrictInt = 8, + ): + widget = PasswordInputWidget( + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 5338a491b..70df6f502 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -26,6 +26,8 @@ MultipleUsersWidget, NumericInput, NumericInputWidget, + PasswordInput, + PasswordInputWidget, ) LABEL: str = "Some label" @@ -1393,3 +1395,96 @@ def test_validation(self, label, required, hidden, help_, placeholder, grid, msg placeholder=placeholder, grid=grid, ) + + +class TestPasswordInput: + def test_constructor_with_defaults(self): + sut = PasswordInput( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, PasswordInputWidget) + assert ui.widget == "password" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = PasswordInput( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, PasswordInputWidget) + assert ui.widget == "password" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, grid, msg", + [ + ( + None, + True, + True, + HELP, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, grid, msg): + with pytest.raises(ValidationError, match=msg): + PasswordInput( + label=label, + required=required, + hidden=hidden, + help_=help_, + grid=grid, + ) From 88415f76eb49e3fa7a37dcf79fac016547e2941e Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:42:24 +0900 Subject: [PATCH 16/56] Add Radio --- pyatlan/pkg/widgets.py | 41 ++++++++++++ tests/unit/pkg/test_widgets.py | 115 +++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 5b832ab0f..a07c19626 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -574,3 +574,44 @@ def __init__( grid=grid, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class RadioWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help_: str = "", + ): + super().__init__( + widget="radio", + label=label, + hidden=hidden, + help_=help_, + ) + + +@dataclass +class Radio(UIElement): + possible_values: dict[str, str] + default: str + + @validate_arguments() + def __init__( + self, + label: StrictStr, + posssible_values: dict[str, str], + default: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + ): + widget = RadioWidget( + label=label, + hidden=hidden, + help_=help_, + ) + super().__init__(type_="string", required=required, ui=widget) + self.possible_values = posssible_values + self.default = default diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 70df6f502..6d138b838 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -28,6 +28,8 @@ NumericInputWidget, PasswordInput, PasswordInputWidget, + Radio, + RadioWidget, ) LABEL: str = "Some label" @@ -1488,3 +1490,116 @@ def test_validation(self, label, required, hidden, help_, grid, msg): help_=help_, grid=grid, ) + + +class TestRadio: + def test_constructor_with_defaults(self): + sut = Radio( + label=LABEL, posssible_values=POSSIBLE_VALUES, default=(default := "a") + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + assert sut.possible_values == POSSIBLE_VALUES + assert sut.default == default + + ui = sut.ui + assert ui + assert isinstance(ui, RadioWidget) + assert ui.widget == "radio" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + + def test_constructor_with_overrides(self): + sut = Radio( + label=LABEL, + posssible_values=POSSIBLE_VALUES, + default=(default := "a"), + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + assert sut.possible_values == POSSIBLE_VALUES + assert sut.default == default + + ui = sut.ui + assert ui + assert isinstance(ui, RadioWidget) + assert ui.widget == "radio" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + + @pytest.mark.parametrize( + "label, possible_values, default, required, hidden, help_, msg", + [ + ( + None, + POSSIBLE_VALUES, + "a", + True, + True, + HELP, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + None, + "a", + True, + True, + HELP, + r"1 validation error for Init\nposssible_values\n none is not an allowed value", + ), + ( + LABEL, + POSSIBLE_VALUES, + None, + True, + True, + HELP, + r"1 validation error for Init\ndefault\n none is not an allowed value", + ), + ( + LABEL, + POSSIBLE_VALUES, + "a", + 1, + True, + HELP, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + POSSIBLE_VALUES, + "a", + True, + 1, + HELP, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + POSSIBLE_VALUES, + "a", + True, + True, + 1, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ], + ) + def test_validation( + self, label, possible_values, default, required, hidden, help_, msg + ): + with pytest.raises(ValidationError, match=msg): + Radio( + label=label, + posssible_values=possible_values, + default=default, + required=required, + hidden=hidden, + help_=help_, + ) From 01ff5a74d236c64f6c713d0985dbfd95829baf68 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:47:33 +0900 Subject: [PATCH 17/56] Add SingleGroup --- pyatlan/pkg/widgets.py | 38 ++++++++++++++ tests/unit/pkg/test_widgets.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index a07c19626..be84808b8 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -615,3 +615,41 @@ def __init__( super().__init__(type_="string", required=required, ui=widget) self.possible_values = posssible_values self.default = default + + +@dataclass +class SingleGroupWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help_: str = "", + grid: int = 8, + ): + super().__init__( + widget="groups", + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + + +@dataclass +class SingleGroup(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + grid: StrictInt = 8, + ): + widget = SingleGroupWidget( + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 6d138b838..6934d9434 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -30,6 +30,8 @@ PasswordInputWidget, Radio, RadioWidget, + SingleGroup, + SingleGroupWidget, ) LABEL: str = "Some label" @@ -1603,3 +1605,96 @@ def test_validation( hidden=hidden, help_=help_, ) + + +class TestSingleGroup: + def test_constructor_with_defaults(self): + sut = SingleGroup( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, SingleGroupWidget) + assert ui.widget == "groups" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = SingleGroup( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, SingleGroupWidget) + assert ui.widget == "groups" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, grid, msg", + [ + ( + None, + True, + True, + HELP, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, grid, msg): + with pytest.raises(ValidationError, match=msg): + SingleGroup( + label=label, + required=required, + hidden=hidden, + help_=help_, + grid=grid, + ) From 1f2bcf6a4057791fb4b30fcdb2145e8107062708 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:52:13 +0900 Subject: [PATCH 18/56] Add SingleUser --- pyatlan/pkg/widgets.py | 38 ++++++++++++++ tests/unit/pkg/test_widgets.py | 95 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index be84808b8..b5c0f5efc 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -653,3 +653,41 @@ def __init__( grid=grid, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class SingleUserWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help_: str = "", + grid: int = 8, + ): + super().__init__( + widget="users", + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + + +@dataclass +class SingleUser(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + grid: StrictInt = 8, + ): + widget = SingleUserWidget( + label=label, + hidden=hidden, + help_=help_, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 6934d9434..7fac4cefa 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -32,6 +32,8 @@ RadioWidget, SingleGroup, SingleGroupWidget, + SingleUser, + SingleUserWidget, ) LABEL: str = "Some label" @@ -1698,3 +1700,96 @@ def test_validation(self, label, required, hidden, help_, grid, msg): help_=help_, grid=grid, ) + + +class TestSingleUser: + def test_constructor_with_defaults(self): + sut = SingleUser( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, SingleUserWidget) + assert ui.widget == "users" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = SingleUser( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, SingleUserWidget) + assert ui.widget == "users" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, grid, msg", + [ + ( + None, + True, + True, + HELP, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, grid, msg): + with pytest.raises(ValidationError, match=msg): + SingleUser( + label=label, + required=required, + hidden=hidden, + help_=help_, + grid=grid, + ) From cdb6b99ee41ccfc015363016a6dc0e2c97c50300 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 15:58:51 +0900 Subject: [PATCH 19/56] Add TextInput --- pyatlan/pkg/widgets.py | 42 ++++++++++++ tests/unit/pkg/test_widgets.py | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index b5c0f5efc..90b14a3e1 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -691,3 +691,45 @@ def __init__( grid=grid, ) super().__init__(type_="string", required=required, ui=widget) + + +@dataclass +class TextInputWidget(Widget): + def __init__( + self, + label: str, + hidden: bool = False, + help_: str = "", + placeholder: str = "", + grid: int = 8, + ): + super().__init__( + widget="input", + label=label, + hidden=hidden, + help_=help_, + placeholder=placeholder, + grid=grid, + ) + + +@dataclass +class TextInput(UIElement): + @validate_arguments() + def __init__( + self, + label: StrictStr, + required: StrictBool = False, + hidden: StrictBool = False, + help_: StrictStr = "", + placeholder: StrictStr = "", + grid: StrictInt = 8, + ): + widget = TextInputWidget( + label=label, + hidden=hidden, + help_=help_, + placeholder=placeholder, + grid=grid, + ) + super().__init__(type_="string", required=required, ui=widget) diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 7fac4cefa..75eab965b 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -34,6 +34,8 @@ SingleGroupWidget, SingleUser, SingleUserWidget, + TextInput, + TextInputWidget, ) LABEL: str = "Some label" @@ -1793,3 +1795,114 @@ def test_validation(self, label, required, hidden, help_, grid, msg): help_=help_, grid=grid, ) + + +class TestTextInput: + def test_constructor_with_defaults(self): + sut = TextInput( + label=LABEL, + ) + assert sut.type_ == "string" + assert sut.required == IS_NOT_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, TextInputWidget) + assert ui.widget == "input" + assert ui.label == LABEL + assert ui.hidden == IS_NOT_HIDDEN + assert ui.help_ == "" + assert ui.placeholder == "" + assert ui.grid == 8 + + def test_constructor_with_overrides(self): + sut = TextInput( + label=LABEL, + required=IS_REQUIRED, + hidden=IS_HIDDEN, + help_=HELP, + placeholder=PLACE_HOLDER, + grid=(grid := 3), + ) + assert sut.type_ == "string" + assert sut.required == IS_REQUIRED + + ui = sut.ui + assert ui + assert isinstance(ui, TextInputWidget) + assert ui.widget == "input" + assert ui.label == LABEL + assert ui.hidden == IS_HIDDEN + assert ui.help_ == HELP + assert ui.placeholder == PLACE_HOLDER + assert ui.grid == grid + + @pytest.mark.parametrize( + "label, required, hidden, help_, placeholder, grid, msg", + [ + ( + None, + True, + True, + HELP, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nlabel\n none is not an allowed value", + ), + ( + LABEL, + 0, + True, + HELP, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nrequired\n value is not a valid boolean", + ), + ( + LABEL, + True, + 0, + HELP, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nhidden\n value is not a valid boolean", + ), + ( + LABEL, + True, + True, + 1, + PLACE_HOLDER, + 3, + r"1 validation error for Init\nhelp_\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + 1, + 3, + r"1 validation error for Init\nplaceholder\n str type expected", + ), + ( + LABEL, + True, + True, + HELP, + PLACE_HOLDER, + "3", + r"1 validation error for Init\ngrid\n value is not a valid integer", + ), + ], + ) + def test_validation(self, label, required, hidden, help_, placeholder, grid, msg): + with pytest.raises(ValidationError, match=msg): + TextInput( + label=label, + required=required, + hidden=hidden, + help_=help_, + placeholder=placeholder, + grid=grid, + ) From 591e6ebaf763aef8897567240fbc9fc494ac5028 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 14 Dec 2023 18:28:59 +0900 Subject: [PATCH 20/56] Add documentation --- pyatlan/pkg/widgets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 90b14a3e1..6610bd26d 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -75,6 +75,18 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 4, ): + """ + Widget that allows you to select an existing API token from a drop-down list, and returns the GUID of the + selected API token. + Note: currently only API tokens that were created by the user configuring the workflow will appear in the + drop-down list. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = APITokenSelectorWidget( label=label, hidden=hidden, help_=help_, grid=grid ) From 369c7e5fa87b91098a50774ddfb99eebda894a6e Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Mon, 18 Dec 2023 13:39:30 +0300 Subject: [PATCH 21/56] Update documentation --- pyatlan/pkg/widgets.py | 175 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 6610bd26d..ba8501207 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -124,6 +124,15 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to choose either "Yes" or "No", and returns the value that was selected. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = BooleanInputWidget(label=label, hidden=hidden, help_=help_, grid=grid) super().__init__(type_="boolean", required=required, ui=widget) @@ -153,6 +162,16 @@ def __init__( help_: StrictStr = "", placeholder: StrictStr = "", ): + """ + Widget that allows you to create a new connection by providing a name and list of admins, and returns a string + representation of the connection object that should be created. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param placeholder: example text to place within the widget to exemplify its use + """ widget = ConnectionCreatorWidget( label=label, hidden=hidden, help_=help_, placeholder=placeholder ) @@ -198,6 +217,18 @@ def __init__( grid: StrictInt = 4, start: StrictInt = 1, ): + """ + Widget that allows you to select an existing connection from a drop-down list, and returns the qualified name + of the selected connection. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param placeholder: example text to place within the widget to exemplify its use + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + :param start: TBC + """ widget = ConnectionSelectorWidget( label=label, hidden=hidden, @@ -244,6 +275,19 @@ def __init__( grid: StrictInt = 4, start: StrictInt = 1, ): + """ + Widget that allows you to select from the types of connectors that exist in the tenant + (for example "Snowflake"), without needing to select a specific instance of a connection + (for example, the "production" connection for Snowflake). Will return a string-encoded object giving the + connection type that was selected and a list of all connections in the tenant that have that type. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + :aram start: TBC + """ widget = ConnectorTypeSelectorWidget( label=label, hidden=hidden, @@ -296,6 +340,23 @@ def __init__( start: StrictInt = 1, grid: StrictInt = 8, ): + """ + Widget that allows you to enter or select a date (not including time) from a calendar, and returns the + epoch-based number representing that selected date in seconds. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param min_: an offset from today (0) that indicates how far back in the calendar can be selected + (-1 is yesterday, 1 is tomorrow, and so on) + :param max_: an offset from today (0) that indicates how far forward in the calendar can be selected + (-1 is yesterday, 1 is tomorrow, and so on) + :param default: an offset from today that indicates the default date that should be selected in the calendar + (0 is today, -1 is yesterday, 1 is tomorrow, and so on) + :param start: TBC + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = DateInputWidget( label=label, hidden=hidden, @@ -346,6 +407,17 @@ def __init__( multi_select: StrictBool = False, grid: StrictInt = 8, ): + """ + Widget that allows you to select from a drop-down of provided options. + + :param label: name to show in the UI for the widget + :param possible_values: map of option keys to the value that will be display for each option in the drop-down + :param required: whether a value must be selected to proceed with the UI setup + :param hidden whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param multi_select: whether multiple options can be selected (true) or only a single option (false) + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = DropDownWidget( label=label, mode="multiple" if multi_select else "", @@ -396,6 +468,17 @@ def __init__( help_: StrictStr = "", placeholder: StrictStr = "", ): + """ + Widget that allows you to upload a file, and returns the GUID-based name of the file (as it is renamed after + upload). + + :param label: name to show in the UI for the widget + :param file_types: list of the mime-types of files that should be accepted + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param placeholder: placeholder example text to place within the widget to exemplify its use + """ widget = FileUploaderWidget( label=label, file_types=file_types, @@ -431,6 +514,17 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to generate a unique key that could be used for securing an exchange or other unique + identification purposes, and provides buttons to regenerate the key or copy its text. Will return the generated + key as clear text. + + :param label: name to show in the UI for the widge + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = KeygenInputWidget( label=label, hidden=hidden, @@ -465,6 +559,15 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to choose multiple groups, and returns an array of group names that were selected. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = MultipleGroupsWidget( label=label, hidden=hidden, @@ -499,6 +602,15 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to choose multiple users, and returns an array of usernames that were selected. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = MultipleUsersWidget( label=label, hidden=hidden, @@ -540,6 +652,17 @@ def __init__( placeholder: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to enter an arbitrary number into a single-line text input field, and returns the value + of the number that was entered. + + :param label name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param placeholder: example text to place within the widget to exemplify its use + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = NumericInputWidget( label=label, hidden=hidden, @@ -579,6 +702,16 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to enter arbitrary text, but the text will be shown as dots when entered rather than + being displayed in clear text. Will return the entered text in clear text. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = PasswordInputWidget( label=label, hidden=hidden, @@ -619,6 +752,18 @@ def __init__( hidden: StrictBool = False, help_: StrictStr = "", ): + """ + Widget that allows you to select just one option from a set of options, and returns the key of the selected + option. Typically, this is used to control mutually exclusive options in the UI. + + :param label: name to show in the UI for the widget + :param possible_values: map of option keys to the value that will be display for each option in the UI + :param default: the default value to select in the UI, given as the string key of the option + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + + """ widget = RadioWidget( label=label, hidden=hidden, @@ -658,6 +803,15 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to select a single group, and returns the group name of the selected group. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = SingleGroupWidget( label=label, hidden=hidden, @@ -696,6 +850,15 @@ def __init__( help_: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to select a single user, and returns the username of the selected user. + + :param label: name to show in the UI for the widget + :param required: whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + """ widget = SingleUserWidget( label=label, hidden=hidden, @@ -737,6 +900,18 @@ def __init__( placeholder: StrictStr = "", grid: StrictInt = 8, ): + """ + Widget that allows you to enter arbitrary text into a single-line text input field, and returns the value of the + text that was entered. + + :param label: name to show in the UI for the widget + :param required" whether a value must be selected to proceed with the UI setup + :param hidden: whether the widget will be shown in the UI (false) or not (true) + :param help_: informational text to place in a hover-over to describe the use of the input + :param placeholder: example text to place within the widget to exemplify its use + :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) + + """ widget = TextInputWidget( label=label, hidden=hidden, From f99be3986aee17fc6f5cafb083e038a35bd4aff2 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 19 Dec 2023 08:26:15 +0300 Subject: [PATCH 22/56] Initial version --- pyatlan/model/assets/asset00.py | 1 - pyatlan/pkg/models.py | 194 ++++++++++++++++++++++++++++++++ pyatlan/pkg/ui.py | 72 ++++++++++++ pyatlan/pkg/utils.py | 42 +++++++ tests/unit/pkg/test_ui.py | 142 +++++++++++++++++++++++ 5 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 pyatlan/pkg/models.py create mode 100644 pyatlan/pkg/ui.py create mode 100644 pyatlan/pkg/utils.py create mode 100644 tests/unit/pkg/test_ui.py diff --git a/pyatlan/model/assets/asset00.py b/pyatlan/model/assets/asset00.py index d4d26d603..94dc78375 100644 --- a/pyatlan/model/assets/asset00.py +++ b/pyatlan/model/assets/asset00.py @@ -390,7 +390,6 @@ def __get_validators__(cls): @classmethod def _convert_to_real_type_(cls, data): - if isinstance(data, Asset): return data diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py new file mode 100644 index 000000000..689b1c9ab --- /dev/null +++ b/pyatlan/pkg/models.py @@ -0,0 +1,194 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Atlan Pte. Ltd. +import json +import logging +import textwrap +from enum import Enum +from pathlib import Path +from typing import Literal, Optional + +from pydantic import BaseModel, Field, PrivateAttr + +from pyatlan.model.enums import AtlanConnectorType +from pyatlan.pkg.ui import UIConfig + +LOGGER = logging.getLogger(__name__) + + +class PackageConfig(BaseModel): + labels: dict[str, str] + annotations: dict[str, str] + + +class _PackageDefinition(BaseModel): + name: str + version: str + description: str + keywords: list[str] + homepage: str + main: str + scripts: dict[str, str] + author: dict[str, str] + repository: dict[str, str] + license: str + bugs: dict[str, str] + config: PackageConfig + + +class ConfigMap(BaseModel): + api_version: Literal["v1"] = "v1" + kind: Literal["ConfigMap"] = "ConfigMap" + metadata: dict[str, str] = Field(default_factory=dict) + data: dict[str, str] = Field(default_factory=dict) + + def __init__(self, name: str, **data): + super().__init__(**data) + self.metadata = {"name": name} + + +class PackageDefinition(BaseModel): + package_id: str + package_name: str + description: str + icon_url: str + docs_url: str + keywords: list[str] = Field(default_factory=list) + scripts: dict[str, str] = Field(default_factory=dict) + allow_schedule: bool = True + certified: bool = True + preview: bool = False + connector_type: Optional[AtlanConnectorType] = None + category: str = "custom" + _package_definition: _PackageDefinition = PrivateAttr() + + def __init__(self, **data): + super().__init__(**data) + source = self.connector_type.value if self.connector_type else "atlan" + source_category = ( + self.connector_type.value if self.connector_type else "utility" + ) + self._package_definition = _PackageDefinition( + name=self.package_id, + version="1.6.5", + description=self.description, + keywords=self.keywords, + homepage=f"https://packages.atlan.com/-/web/detail/{self.package_id}", + main="index.js", + scripts=self.scripts, + author={ + "name": "Atlan CSA", + "email": "csa@atlan.com", + "url": "https://atlan.com", + }, + repository={ + "type": "git", + "url": "https://github.com/atlanhq/marketplace-packages.git", + }, + license="MIT", + bugs={ + "url": "https://atlan.com", + "email": "support@atlan.com", + }, + config=PackageConfig( + labels={ + "orchestration.atlan.com/verified": "true", + "orchestration.atlan.com/type": self.category, + "orchestration.atlan.com/source": source, + "orchestration.atlan.com/sourceCategory": source_category, + "orchestration.atlan.com/certified": str(self.certified).lower(), + "orchestration.atlan.com/preview": str(self.preview).lower(), + }, + annotations={ + "orchestration.atlan.com/name": self.package_name, + "orchestration.atlan.com/allowSchedule": str( + self.allow_schedule + ).lower(), + "orchestration.atlan.com/dependentPackage": "", + "orchestration.atlan.com/emoji": "🚀", + "orchestration.atlan.com/categories": ",".join(self.keywords), + "orchestration.atlan.com/icon": self.icon_url, + "orchestration.atlan.com/logo": self.icon_url, + "orchestration.atlan.com/docsUrl": self.docs_url, + }, + ), + ) + + @property + def packageJSON(self): + json_object = json.loads(self._package_definition.json()) + return json.dumps(json_object, indent=2, ensure_ascii=False) + + +class PullPolicy(str, Enum): + ALWAYS = "Always" + IF_NOT_PRESENT = "IfNotPresent" + + +class CustomPackage(BaseModel): + package_id: str + package_name: str + description: str + icon_url: str + docs_url: str + ui_config: UIConfig + keywords: list[str] = Field(default_factory=list) + container_image: str + container_image_pull_policy: PullPolicy = PullPolicy.IF_NOT_PRESENT + container_command: list[str] + allow_schedule: bool = True + certified: bool = True + preview: bool = False + connector_type: Optional[AtlanConnectorType] = None + category: str = "custom" + _pkg: PackageDefinition = PrivateAttr() + _name: str = PrivateAttr() + + def _init_private_attributes(self) -> None: + super()._init_private_attributes() + self._pkg = PackageDefinition( + package_id=self.package_id, + package_name=self.package_name, + description=self.description, + icon_url=self.icon_url, + docs_url=self.docs_url, + keywords=self.keywords, + allow_schedule=self.allow_schedule, + certified=self.certified, + preview=self.preview, + connector_type=self.connector_type, + category=self.category, + ) + self._name = self.package_id.replace("@", "").replace("/", "-") + + @property + def name(self) -> str: + return self._name + + @property + def packageJSON(self) -> str: + return self._pkg.packageJSON + + @staticmethod + def indexJS() -> str: + return textwrap.dedent( + """\ + function dummy() { + console.log("don't call this.") + } + module.exports = dummy; + """ + ) + + @staticmethod + def create_package(pkg: "CustomPackage", args: list[str]): + path = args[0] + root_dir = Path(path) / pkg.name + root_dir.mkdir(parents=True, exist_ok=True) + with (root_dir / "index.js").open("w") as index: + index.write(CustomPackage.indexJS()) + with (root_dir / "package.json").open("w") as package: + package.write(pkg.packageJSON) + config_maps_dir = root_dir / "configmaps" + config_maps_dir.mkdir(parents=True, exist_ok=True) + templates_dir = root_dir / "templates" + templates_dir.mkdir(parents=True, exist_ok=True) diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py new file mode 100644 index 000000000..eb1c261f1 --- /dev/null +++ b/pyatlan/pkg/ui.py @@ -0,0 +1,72 @@ +import logging +from dataclasses import dataclass, field +from typing import Any + +from pydantic import StrictStr, validate_arguments + +from pyatlan.pkg.widgets import UIElement + +LOGGER = logging.getLogger(__name__) + + +@dataclass() +class UIStep: + title: str + inputs: dict[str, UIElement] + description: str = "" + id: str = "" + properties: list[str] = field(default_factory=list) + + @validate_arguments() + def __init__( + self, + title: StrictStr, + inputs: dict[StrictStr, UIElement], + description: StrictStr = "", + ): + self.title = title + self.inputs = inputs + self.description = description + self.id = title.replace(" ", "_").lower() + self.properties = list(self.inputs.keys()) + + +@dataclass() +class UIRule: + when_inputs: dict[str, str] + required: list[str] + properties: dict[str, dict[str, str]] = field(default_factory=dict) + + @validate_arguments() + def __init__( + self, when_inputs: dict[StrictStr, StrictStr], required: list[StrictStr] + ): + """ + Configure basic UI rules that when the specified inputs have specified values, certain other fields become + required. + + :param when_inputs: mapping from input ID to value for the step + :param required: list of input IDs that should become required when the inputs all match + + """ + self.when_inputs = when_inputs + self.required = required + self.properties = {key: {"const": value} for key, value in when_inputs.items()} + + +@dataclass() +class UIConfig: + steps: list[UIStep] + rules: list[Any] + properties: dict[str, UIElement] = field(default_factory=dict) + + @validate_arguments() + def __init__(self, steps: list[UIStep], rules: list[Any]): + self.steps = steps + self.rules = rules + self.properties = {} + for step in steps: + for key, value in step.inputs.items(): + if key in self.properties: + LOGGER.warning("Duplicate key found accross steps: %s", key) + self.properties[key] = value diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py new file mode 100644 index 000000000..5365b6a26 --- /dev/null +++ b/pyatlan/pkg/utils.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Atlan Pte. Ltd. +from pathlib import Path + +from jinja2 import Environment, PackageLoader +from pydantic import BaseModel, PrivateAttr + +from pyatlan.pkg.models import CustomPackage + + +class PackageWriter(BaseModel): + path: str + pkg: CustomPackage + _root_dir: Path = PrivateAttr() + _config_maps_dir: Path = PrivateAttr() + _templates_dir: Path = PrivateAttr() + _env: Environment = PrivateAttr() + + def __init__(self, **data): + super().__init__(**data) + self._root_dir = Path(self.path) / self.pkg.name + self._config_maps_dir = self._root_dir / "configmaps" + self._templates_dir = self._root_dir / "templates" + self._env = Environment( # noqa: S701 + loader=PackageLoader("package_toolkit.pkg", "templates") + ) + + def create_package(self): + self._root_dir.mkdir(parents=True, exist_ok=True) + with (self._root_dir / "index.js").open("w") as index: + index.write(CustomPackage.indexJS()) + with (self._root_dir / "index.js").open("w") as index: + index.write(CustomPackage.indexJS()) + with (self._root_dir / "package.json").open("w") as package: + package.write(self.pkg.packageJSON) + + def create_templates(self): + self._templates_dir.mkdir(parents=True, exist_ok=True) + template = self._env.get_template("default_template.jinja2") + content = template.render({"pkg": self.pkg}) + with (self._templates_dir / "default.yaml").open("w") as script: + script.write(content) diff --git a/tests/unit/pkg/test_ui.py b/tests/unit/pkg/test_ui.py new file mode 100644 index 000000000..81b325d02 --- /dev/null +++ b/tests/unit/pkg/test_ui.py @@ -0,0 +1,142 @@ +import pytest +from pydantic import ValidationError + +from pyatlan.pkg.ui import UIConfig, UIRule, UIStep +from pyatlan.pkg.widgets import TextInput, UIElement + +WHEN_INPUTS_VALUE = "advanced" + +WHEN_INPUTS_KEY = "control_config_strategy" + +REQUIRED = ["asset_types"] + +WHEN_INPUTS = {WHEN_INPUTS_KEY: WHEN_INPUTS_VALUE} + +TITLE = "Some Title" +DESCRIPTION = "Some description" + + +@pytest.fixture() +def text_input() -> TextInput: + return TextInput(label="Qualified name prefix") + + +@pytest.fixture() +def inputs(text_input: TextInput) -> dict[str, UIElement]: + return {"qn_prefix": text_input} + + +@pytest.fixture() +def ui_step(inputs) -> UIStep: + return UIStep(title=TITLE, inputs=inputs) + + +@pytest.fixture() +def ui_rule() -> UIRule: + return UIRule(when_inputs=WHEN_INPUTS, required=REQUIRED) + + +class TestUIConfig: + def test_constructor(self, ui_step, ui_rule): + sut = UIConfig(steps=[ui_step], rules=[ui_rule]) + assert sut.rules == [ui_rule] + assert sut.steps == [ui_step] + for key, value in ui_step.inputs.items(): + assert key in sut.properties + assert sut.properties[key] == value + + # @pytest.mark.parametrize( + # "ui_step, ui_rule, msg", + # [({1: "kjk"}, REQUIRED, r"1 validation error for Init\nwhen_inputs -> __key__\n str type expected"), + # (WHEN_INPUTS, [1], r"1 validation error for Init\nrequired -> 0\n str type expected") + # ], + # ) + # def test_validation(self, ui_step, ui_rule, msg): + # with pytest.raises(ValidationError, match=msg): + # UIConfig(ui_step=ui_stpe) + + +class TestUIRule: + def test_constructor(self, text_input, inputs): + sut = UIRule(when_inputs=WHEN_INPUTS, required=REQUIRED) + assert sut.properties == {WHEN_INPUTS_KEY: {"const": WHEN_INPUTS_VALUE}} + assert sut.required == REQUIRED + assert sut.when_inputs == WHEN_INPUTS + + @pytest.mark.parametrize( + "when_inputs, required, msg", + [ + ( + {1: "kjk"}, + REQUIRED, + r"1 validation error for Init\nwhen_inputs -> __key__\n str type expected", + ), + ( + WHEN_INPUTS, + [1], + r"1 validation error for Init\nrequired -> 0\n str type expected", + ), + ], + ) + def test_validation(self, when_inputs, required, msg): + with pytest.raises(ValidationError, match=msg): + UIRule(when_inputs=when_inputs, required=required) + + +class TestUIStep: + def test_constructor_with_defaults(self, text_input, inputs): + sut = UIStep(title=TITLE, inputs=inputs) + + assert sut.title == TITLE + assert sut.inputs == inputs + assert sut.description == "" + assert sut.id == TITLE.replace(" ", "_").lower() + assert sut.properties == list(inputs.keys()) + + def test_constructor_with_overrides(self, text_input, inputs): + sut = UIStep(title=TITLE, inputs=inputs, description=DESCRIPTION) + + assert sut.title == TITLE + assert sut.inputs == inputs + assert sut.description == DESCRIPTION + assert sut.id == TITLE.replace(" ", "_").lower() + assert sut.properties == list(inputs.keys()) + + @pytest.mark.parametrize( + "title, input, description, msg", + [ + ( + None, + {"qn_prefix": TextInput(label="Qualified name prefix")}, + "", + r"1 validation error for Init\ntitle\n none is not an allowed value", + ), + ( + 1, + {"qn_prefix": TextInput(label="Qualified name prefix")}, + "", + r"1 validation error for Init\ntitle\n str type expected", + ), + ( + TITLE, + {"qn_prefix": "oioi"}, + "", + r"1 validation error for Init\ninputs -> qn_prefix\n instance of UIElement", + ), + ( + TITLE, + {"qn_prefix": TextInput(label="Qualified name prefix")}, + None, + r"1 validation error for Init\ndescription\n none is not an allowed value", + ), + ( + TITLE, + {"qn_prefix": TextInput(label="Qualified name prefix")}, + 1, + r"1 validation error for Init\ndescription\n str type expected", + ), + ], + ) + def test_validation(self, title, input, description, msg): + with pytest.raises(ValidationError, match=msg): + UIStep(title=title, inputs=input, description=description) From 5e1b523799e996c9937dbc44b73c81e6efcedfec Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 19 Dec 2023 09:32:42 +0300 Subject: [PATCH 23/56] Add unit test --- tests/unit/pkg/test_ui.py | 44 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/tests/unit/pkg/test_ui.py b/tests/unit/pkg/test_ui.py index 81b325d02..400f1a449 100644 --- a/tests/unit/pkg/test_ui.py +++ b/tests/unit/pkg/test_ui.py @@ -36,6 +36,22 @@ def ui_rule() -> UIRule: return UIRule(when_inputs=WHEN_INPUTS, required=REQUIRED) +@pytest.fixture() +def good_or_bad_step(request, ui_step): + if request.param == "good": + return [ui_step] + else: + return None + + +@pytest.fixture() +def good_or_bad_rule(request, ui_rule): + if request.param == "good": + return [ui_rule] + else: + return None + + class TestUIConfig: def test_constructor(self, ui_step, ui_rule): sut = UIConfig(steps=[ui_step], rules=[ui_rule]) @@ -45,15 +61,25 @@ def test_constructor(self, ui_step, ui_rule): assert key in sut.properties assert sut.properties[key] == value - # @pytest.mark.parametrize( - # "ui_step, ui_rule, msg", - # [({1: "kjk"}, REQUIRED, r"1 validation error for Init\nwhen_inputs -> __key__\n str type expected"), - # (WHEN_INPUTS, [1], r"1 validation error for Init\nrequired -> 0\n str type expected") - # ], - # ) - # def test_validation(self, ui_step, ui_rule, msg): - # with pytest.raises(ValidationError, match=msg): - # UIConfig(ui_step=ui_stpe) + @pytest.mark.parametrize( + "good_or_bad_step, good_or_bad_rule, msg", + [ + ( + "good", + "bad", + r"1 validation error for Init\nrules\n none is not an allowed value", + ), + ( + "bad", + "good", + r"1 validation error for Init\nsteps\n none is not an allowed value", + ), + ], + indirect=["good_or_bad_step", "good_or_bad_rule"], + ) + def test_validation(self, good_or_bad_step, good_or_bad_rule, msg): + with pytest.raises(ValidationError, match=msg): + UIConfig(steps=good_or_bad_step, rules=good_or_bad_rule) class TestUIRule: From 1dfd0309ae644f2ccffa006579dd724a4aac3c8b Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 20 Dec 2023 17:03:43 +0300 Subject: [PATCH 24/56] Refactor to fix validation --- pyatlan/pkg/models.py | 27 ++- pyatlan/pkg/templates/default_template.jinja2 | 48 +++++ pyatlan/pkg/ui.py | 42 +++- pyatlan/pkg/utils.py | 2 +- pyatlan/pkg/widgets.py | 113 ++++++----- tests/integration/custom_package_test.py | 36 ++++ tests/unit/pkg/conftest.py | 179 ++++++++++++++++++ tests/unit/pkg/test_models.py | 27 +++ tests/unit/pkg/test_ui.py | 36 +++- tests/unit/pkg/test_widgets.py | 4 +- 10 files changed, 458 insertions(+), 56 deletions(-) create mode 100644 pyatlan/pkg/templates/default_template.jinja2 create mode 100644 tests/integration/custom_package_test.py create mode 100644 tests/unit/pkg/conftest.py create mode 100644 tests/unit/pkg/test_models.py diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 689b1c9ab..30320d405 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Literal, Optional -from pydantic import BaseModel, Field, PrivateAttr +from pydantic import BaseModel, Field, PrivateAttr, validate_arguments from pyatlan.model.enums import AtlanConnectorType from pyatlan.pkg.ui import UIConfig @@ -46,6 +46,31 @@ def __init__(self, name: str, **data): self.metadata = {"name": name} +class NamePathS3Tuple: + name: str + path: str + s3: dict[str, str] + + def __init__(self, input_name: str): + self.name = f"{input_name}_s3" + self.path = ( + f"/tmp/{input_name}/{{{{inputs.parameters.{input_name}}}}}" # noqa: S108 + ) + self.s3 = {"key": f"{{{{inputs.parameters.{input_name}}}}}"} + + +class WorkflowInputs: + parameters: list[tuple[str, str]] + artifacts: list[NamePathS3Tuple] + + @validate_arguments() + def __init__(self, config: UIConfig, pkg_name: str): + self.parameters = [] + self.artifacts = [] + for _key, value in config.properties.items(): + value.ui + + class PackageDefinition(BaseModel): package_id: str package_name: str diff --git a/pyatlan/pkg/templates/default_template.jinja2 b/pyatlan/pkg/templates/default_template.jinja2 new file mode 100644 index 000000000..c0886f648 --- /dev/null +++ b/pyatlan/pkg/templates/default_template.jinja2 @@ -0,0 +1,48 @@ +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: {{ pkg.name }} +spec: + templates: + - name: main + inputs: + parameters: + {%- if pkg.name %} + - name: output_prefix + value: {{ pkg.name }} + {%- endif %} + artifacts: + outputs: + artifacts: + container: + image: {{ pkg.container_image }} + imagePullPolicy: {{ pkg.container_image_pull_policy.value }} + command: + - {{ pkg.container_command[0] }} + args: + {%- for cmd in pkg.container_command[1:] %} + - {{ cmd }} + {%- endfor %} + env: + - name: ATLAN_BASE_URL + value: INTERNAL + - name: ATLAN_USER_ID + value: "{% raw %}{{=sprig.dig('labels', 'workflows', 'argoproj', 'io/creator', '', workflow)}}{% endraw %}" + - name: X_ATLAN_AGENT + value: workflow + - name: X_ATLAN_AGENT_ID + value: "{% raw %}{{workflow.name}}{% endraw %}" + - name: X_ATLAN_AGENT_PACKAGE_NAME + value: "{% raw %}{{=sprig.dig('annotations', 'package', 'argoproj', 'io/name', '', workflow)}}{% endraw %}" + - name: X_ATLAN_AGENT_WORKFLOW_ID + value: "{% raw %}{{=sprig.dig('labels', 'workflows', 'argoproj', 'io/workflow-template', '', workflow)}}{% endraw %}" + - name: CLIENT_ID + valueFrom: + secretKeyRef: + name: argo-client-creds + key: login + - name: CLIENT_SECRET + valueFrom: + secretKeyRef: + name: argo-client-creds + key: password diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py index eb1c261f1..6c542bd7b 100644 --- a/pyatlan/pkg/ui.py +++ b/pyatlan/pkg/ui.py @@ -1,13 +1,49 @@ import logging from dataclasses import dataclass, field -from typing import Any +from typing import Any, Union from pydantic import StrictStr, validate_arguments -from pyatlan.pkg.widgets import UIElement +from pyatlan.pkg.widgets import ( + APITokenSelector, + BooleanInput, + ConnectionCreator, + ConnectorTypeSelector, + DateInput, + DropDown, + FileUploader, + KeygenInput, + MultipleGroups, + MultipleUsers, + NumericInput, + PasswordInput, + Radio, + SingleGroup, + SingleUser, + TextInput, +) LOGGER = logging.getLogger(__name__) +UIElement = Union[ + APITokenSelector, + BooleanInput, + ConnectionCreator, + ConnectorTypeSelector, + DateInput, + DropDown, + FileUploader, + KeygenInput, + MultipleGroups, + MultipleUsers, + NumericInput, + PasswordInput, + Radio, + SingleGroup, + SingleUser, + TextInput, +] + @dataclass() class UIStep: @@ -17,7 +53,7 @@ class UIStep: id: str = "" properties: list[str] = field(default_factory=list) - @validate_arguments() + @validate_arguments(config=dict(arbitrary_types_allowed=True)) def __init__( self, title: StrictStr, diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index 5365b6a26..b79be790b 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -22,7 +22,7 @@ def __init__(self, **data): self._config_maps_dir = self._root_dir / "configmaps" self._templates_dir = self._root_dir / "templates" self._env = Environment( # noqa: S701 - loader=PackageLoader("package_toolkit.pkg", "templates") + loader=PackageLoader("pyatlan.pkg", "templates") ) def create_package(self): diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index ba8501207..94affb0be 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -4,15 +4,35 @@ from dataclasses import dataclass, field # from dataclasses import dataclass -from typing import Optional +from typing import Optional, Union from pydantic import StrictBool, StrictInt, StrictStr, validate_arguments # from pydantic.dataclasses import dataclass - -@dataclass -class Widget(abc.ABC): +Widget = Union[ + "APITokenSelectorWidget", + "BooleanInputWidget", + "ConnectionCreatorWidget", + "ConnectionSelectorWidget", + "ConnectorTypeSelectorWidget", + "DateInputWidget", + "DropDownWidget", + "FileUploaderWidget", + "KeygenInputWidget", + "MultipleGroupsWidget", + "MultipleUsersWidget", + "NumericInputWidget", + "PasswordInputWidget", + "RadioWidget", + "SingleGroupWidget", + "SingleUserWidget", + "TextInputWidget", +] + + +@dataclass +class AbstractWidget(abc.ABC): widget: str label: str hidden: bool = False @@ -22,17 +42,17 @@ class Widget(abc.ABC): @dataclass -class UIElement(abc.ABC): +class AbstractUIElement(abc.ABC): type_: str required: bool ui: Optional[Widget] @dataclass -class UIElementWithEnum(UIElement): - default: Optional[str] +class UIElementWithEnum(AbstractUIElement): enum: list[str] enum_names: list[str] + default: Optional[str] = None def __init__( self, @@ -47,7 +67,7 @@ def __init__( @dataclass -class APITokenSelectorWidget(Widget): +class APITokenSelectorWidget(AbstractWidget): def __init__( self, label: str, @@ -65,7 +85,7 @@ def __init__( @dataclass -class APITokenSelector(UIElement): +class APITokenSelector(AbstractUIElement): @validate_arguments() def __init__( self, @@ -94,7 +114,7 @@ def __init__( @dataclass -class BooleanInputWidget(Widget): +class BooleanInputWidget(AbstractWidget): def __init__( self, label: str, @@ -114,7 +134,7 @@ def __init__( @dataclass -class BooleanInput(UIElement): +class BooleanInput(AbstractUIElement): @validate_arguments() def __init__( self, @@ -138,7 +158,7 @@ def __init__( @dataclass -class ConnectionCreatorWidget(Widget): +class ConnectionCreatorWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, help_: str = "", placeholder: str = "" ): @@ -152,7 +172,7 @@ def __init__( @dataclass -class ConnectionCreator(UIElement): +class ConnectionCreator(AbstractUIElement): @validate_arguments() def __init__( self, @@ -179,7 +199,7 @@ def __init__( @dataclass -class ConnectionSelectorWidget(Widget): +class ConnectionSelectorWidget(AbstractWidget): start: int = 1 def __init__( @@ -203,7 +223,7 @@ def __init__( @dataclass -class ConnectionSelector(UIElement): +class ConnectionSelector(AbstractUIElement): start: StrictInt = 1 @validate_arguments() @@ -242,7 +262,7 @@ def __init__( @dataclass -class ConnectorTypeSelectorWidget(Widget): +class ConnectorTypeSelectorWidget(AbstractWidget): start: int = 1 def __init__( @@ -264,7 +284,7 @@ def __init__( @dataclass -class ConnectorTypeSelector(UIElement): +class ConnectorTypeSelector(AbstractUIElement): @validate_arguments() def __init__( self, @@ -299,7 +319,7 @@ def __init__( @dataclass -class DateInputWidget(Widget): +class DateInputWidget(AbstractWidget): min_: int = -14 max_: int = 0 default: int = 0 @@ -326,7 +346,7 @@ def __init__( @dataclass -class DateInput(UIElement): +class DateInput(AbstractUIElement): @validate_arguments() def __init__( self, @@ -371,7 +391,7 @@ def __init__( @dataclass -class DropDownWidget(Widget): +class DropDownWidget(AbstractWidget): mode: str = "" def __init__( @@ -394,7 +414,7 @@ def __init__( @dataclass class DropDown(UIElementWithEnum): - possible_values: dict[str, str] + possible_values: dict[str, str] = field(default_factory=dict) @validate_arguments() def __init__( @@ -435,7 +455,7 @@ def __init__( @dataclass -class FileUploaderWidget(Widget): +class FileUploaderWidget(AbstractWidget): file_types: list[str] = field(default_factory=list) def __init__( @@ -457,7 +477,7 @@ def __init__( @dataclass -class FileUploader(UIElement): +class FileUploader(AbstractUIElement): @validate_arguments() def __init__( self, @@ -490,7 +510,7 @@ def __init__( @dataclass -class KeygenInputWidget(Widget): +class KeygenInputWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 ): @@ -504,7 +524,7 @@ def __init__( @dataclass -class KeygenInput(UIElement): +class KeygenInput(AbstractUIElement): @validate_arguments() def __init__( self, @@ -535,7 +555,7 @@ def __init__( @dataclass -class MultipleGroupsWidget(Widget): +class MultipleGroupsWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 ): @@ -549,7 +569,7 @@ def __init__( @dataclass -class MultipleGroups(UIElement): +class MultipleGroups(AbstractUIElement): @validate_arguments() def __init__( self, @@ -578,7 +598,7 @@ def __init__( @dataclass -class MultipleUsersWidget(Widget): +class MultipleUsersWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 ): @@ -592,7 +612,7 @@ def __init__( @dataclass -class MultipleUsers(UIElement): +class MultipleUsers(AbstractUIElement): @validate_arguments() def __init__( self, @@ -621,7 +641,7 @@ def __init__( @dataclass -class NumericInputWidget(Widget): +class NumericInputWidget(AbstractWidget): def __init__( self, label: str, @@ -641,7 +661,7 @@ def __init__( @dataclass -class NumericInput(UIElement): +class NumericInput(AbstractUIElement): @validate_arguments() def __init__( self, @@ -674,7 +694,7 @@ def __init__( @dataclass -class PasswordInputWidget(Widget): +class PasswordInputWidget(AbstractWidget): def __init__( self, label: str, @@ -692,7 +712,7 @@ def __init__( @dataclass -class PasswordInput(UIElement): +class PasswordInput(AbstractUIElement): @validate_arguments() def __init__( self, @@ -722,7 +742,7 @@ def __init__( @dataclass -class RadioWidget(Widget): +class RadioWidget(AbstractWidget): def __init__( self, label: str, @@ -738,10 +758,7 @@ def __init__( @dataclass -class Radio(UIElement): - possible_values: dict[str, str] - default: str - +class Radio(UIElementWithEnum): @validate_arguments() def __init__( self, @@ -769,13 +786,17 @@ def __init__( hidden=hidden, help_=help_, ) - super().__init__(type_="string", required=required, ui=widget) - self.possible_values = posssible_values + super().__init__( + type_="string", + required=required, + ui=widget, + possible_values=posssible_values, + ) self.default = default @dataclass -class SingleGroupWidget(Widget): +class SingleGroupWidget(AbstractWidget): def __init__( self, label: str, @@ -793,7 +814,7 @@ def __init__( @dataclass -class SingleGroup(UIElement): +class SingleGroup(AbstractUIElement): @validate_arguments() def __init__( self, @@ -822,7 +843,7 @@ def __init__( @dataclass -class SingleUserWidget(Widget): +class SingleUserWidget(AbstractWidget): def __init__( self, label: str, @@ -840,7 +861,7 @@ def __init__( @dataclass -class SingleUser(UIElement): +class SingleUser(AbstractUIElement): @validate_arguments() def __init__( self, @@ -869,7 +890,7 @@ def __init__( @dataclass -class TextInputWidget(Widget): +class TextInputWidget(AbstractWidget): def __init__( self, label: str, @@ -889,7 +910,7 @@ def __init__( @dataclass -class TextInput(UIElement): +class TextInput(AbstractUIElement): @validate_arguments() def __init__( self, diff --git a/tests/integration/custom_package_test.py b/tests/integration/custom_package_test.py new file mode 100644 index 000000000..66e8414ae --- /dev/null +++ b/tests/integration/custom_package_test.py @@ -0,0 +1,36 @@ +from pyatlan.pkg.models import CustomPackage +from pyatlan.pkg.ui import UIConfig, UIStep +from pyatlan.pkg.utils import PackageWriter +from pyatlan.pkg.widgets import TextInput + + +def test_custom_package(): + pkg = CustomPackage( + package_id="@csa/owner-propagator", + package_name="Owner Propagator", + description="Propagate owners from schema downwards.", + icon_url="https://assets.atlan.com/assets/ph-user-switch-light.svg", + docs_url="https://solutions.atlan.com/", + ui_config=UIConfig( + steps=[ + UIStep( + title="Configuration", + description="Owner propagation configuration", + inputs={ + "qn_prefix": TextInput( + label="Qualified name prefix", + help_="Provide the starting name for schemas from which to propagate ownership", + required=False, + placeholder="default/snowflake/1234567890", + grid=4, + ) + }, + ) + ] + ), + container_image="ghcr.io/atlanhq/csa-owner-propagator:123", + container_command=["/dumb-init", "--", "java", "OwnerPropagator"], + ) + writer = PackageWriter(path="../../generated_packages", pkg=pkg) + writer.create_package() + writer.create_templates() diff --git a/tests/unit/pkg/conftest.py b/tests/unit/pkg/conftest.py new file mode 100644 index 000000000..a618f8aaa --- /dev/null +++ b/tests/unit/pkg/conftest.py @@ -0,0 +1,179 @@ +import pytest + +from pyatlan.pkg.ui import UIStep +from pyatlan.pkg.widgets import ( + APITokenSelector, + BooleanInput, + ConnectionCreator, + ConnectorTypeSelector, + DateInput, + DropDown, + FileUploader, + KeygenInput, + MultipleGroups, + MultipleUsers, + NumericInput, + PasswordInput, + Radio, + SingleGroup, + SingleUser, + TextInput, +) + +LABEL = "Some label" +HELP = "some help" +PLACEHOLDER = "some placeholder" +TITLE = "some title" +DESCRIPTION = "some description" +POSSIBLE_VALUES = {"name": "Dave"} + + +@pytest.fixture() +def api_token_selector() -> APITokenSelector: + return APITokenSelector(label=LABEL) + + +@pytest.fixture() +def boolean_input() -> BooleanInput: + return BooleanInput(label=LABEL) + + +@pytest.fixture() +def connection_creator() -> ConnectionCreator: + return ConnectionCreator(label=LABEL) + + +@pytest.fixture() +def connector_type_selector() -> ConnectorTypeSelector: + return ConnectorTypeSelector(label=LABEL) + + +@pytest.fixture() +def date_input() -> DateInput: + return DateInput(label=LABEL) + + +@pytest.fixture() +def drop_down() -> DropDown: + return DropDown(label=LABEL, possible_values=POSSIBLE_VALUES) + + +@pytest.fixture() +def file_uploader() -> FileUploader: + uploader = FileUploader( + label=LABEL, + file_types=["text/csv"], + required=False, + help_=HELP, + placeholder=PLACEHOLDER, + ) + return uploader + + +@pytest.fixture() +def keygen_input() -> KeygenInput: + return KeygenInput(label=LABEL) + + +@pytest.fixture() +def multiple_groups() -> MultipleGroups: + return MultipleGroups(label=LABEL) + + +@pytest.fixture() +def multiple_users() -> MultipleUsers: + return MultipleUsers(label=LABEL) + + +@pytest.fixture() +def numeric_input() -> NumericInput: + return NumericInput(label=LABEL) + + +@pytest.fixture() +def password_input() -> PasswordInput: + return PasswordInput(label=LABEL) + + +@pytest.fixture() +def radio() -> Radio: + return Radio(label=LABEL, posssible_values=POSSIBLE_VALUES, default="one") + + +@pytest.fixture() +def single_group() -> SingleGroup: + return SingleGroup(label=LABEL) + + +@pytest.fixture() +def single_user() -> SingleUser: + return SingleUser(label=LABEL) + + +@pytest.fixture() +def text_input() -> TextInput: + return TextInput(label="Qualified name prefix") + + +@pytest.fixture() +def an_input( + request, + api_token_selector, + boolean_input, + connection_creator, + connector_type_selector, + date_input, + drop_down, + file_uploader, + keygen_input, + multiple_groups, + multiple_users, + numeric_input, + password_input, + radio, + single_group, + single_user, + text_input, +): + if request.param == "APITokenSelector": + return api_token_selector + if request.param == "BooleanInput": + return boolean_input + if request.param == "TextInput": + return text_input + if request.param == "ConnectionCreator": + return connection_creator + if request.param == "ConnectionSelector": + return connector_type_selector + if request.param == "ConnectorTypeSelector": + return connector_type_selector + if request.param == "DateInput": + return date_input + if request.param == "DropDown": + return drop_down + if request.param == "FileUploader": + return file_uploader + if request.param == "KeygenInput": + return keygen_input + if request.param == "MultipleGroups": + return multiple_groups + if request.param == "MultipleUsers": + return multiple_users + if request.param == "NumericInput": + return numeric_input + if request.param == "PasswordInput": + return password_input + if request.param == "Radio": + return radio + if request.param == "SingleGroup": + return single_group + if request.param == "SingleUser": + return single_user + if request.param == "TextInput": + return text_input + return None + + +@pytest.fixture() +def ui_step(text_input) -> UIStep: + return UIStep(title=TITLE, inputs={"key": text_input}) diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py new file mode 100644 index 000000000..3fb7f312d --- /dev/null +++ b/tests/unit/pkg/test_models.py @@ -0,0 +1,27 @@ +from pyatlan.pkg.models import NamePathS3Tuple + +LABEL = "Some label" +HELP = "some help" +PLACEHOLDER = "some placeholder" +TITLE = "some title" +DESCRIPTION = "some description" + + +class TestNamePathS3Tuple: + def test_constructor(self): + sut = NamePathS3Tuple(input_name="something") + + assert sut.name == "something_s3" + assert sut.path == "/tmp/something/{{inputs.parameters.something}}" + assert sut.s3 == {"key": "{{inputs.parameters.something}}"} + + +class TestWorkflowInputs: + def test_constructor(self, ui_step): + pass + # config = UIConfig(steps=[ + # UIStep(title="Configuration", + # description="Owner propagation configuration", + # inputs={"assets_file": file_uploader}) + # ]) + # sut = WorkflowInputs(config=config, pkg_name="bob")bob diff --git a/tests/unit/pkg/test_ui.py b/tests/unit/pkg/test_ui.py index 400f1a449..fd67205f8 100644 --- a/tests/unit/pkg/test_ui.py +++ b/tests/unit/pkg/test_ui.py @@ -2,7 +2,7 @@ from pydantic import ValidationError from pyatlan.pkg.ui import UIConfig, UIRule, UIStep -from pyatlan.pkg.widgets import TextInput, UIElement +from pyatlan.pkg.widgets import AbstractUIElement, TextInput WHEN_INPUTS_VALUE = "advanced" @@ -14,6 +14,9 @@ TITLE = "Some Title" DESCRIPTION = "Some description" +HELP = "some help" +PLACEHOLDER = "some placeholder" +LABEL = "Some label" @pytest.fixture() @@ -22,7 +25,7 @@ def text_input() -> TextInput: @pytest.fixture() -def inputs(text_input: TextInput) -> dict[str, UIElement]: +def inputs(text_input: TextInput) -> dict[str, AbstractUIElement]: return {"qn_prefix": text_input} @@ -110,6 +113,32 @@ def test_validation(self, when_inputs, required, msg): class TestUIStep: + @pytest.mark.parametrize( + "an_input", + [ + "APITokenSelector", + "BooleanInput", + "ConnectionCreator", + "ConnectionSelector", + "ConnectorTypeSelector", + "DateInput", + "DropDown", + "FileUploader", + "KeygenInput", + "MultipleGroups", + "MultipleUsers", + "NumericInput", + "PasswordInput", + "Radio", + "SingleGroup", + "SingleUser", + "TextInput", + ], + indirect=True, + ) + def test_contstructor(self, an_input): + UIStep(title=TITLE, inputs={"an_input": an_input}) + def test_constructor_with_defaults(self, text_input, inputs): sut = UIStep(title=TITLE, inputs=inputs) @@ -147,7 +176,8 @@ def test_constructor_with_overrides(self, text_input, inputs): TITLE, {"qn_prefix": "oioi"}, "", - r"1 validation error for Init\ninputs -> qn_prefix\n instance of UIElement", + r"16 validation errors for Init\ninputs -> qn_prefix\n instance of APITokenSelector, tuple or dict " + r"expected", ), ( TITLE, diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 75eab965b..6cc90edf5 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -1505,7 +1505,7 @@ def test_constructor_with_defaults(self): ) assert sut.type_ == "string" assert sut.required == IS_NOT_REQUIRED - assert sut.possible_values == POSSIBLE_VALUES + # assert sut.possible_values == POSSIBLE_VALUES assert sut.default == default ui = sut.ui @@ -1527,7 +1527,7 @@ def test_constructor_with_overrides(self): ) assert sut.type_ == "string" assert sut.required == IS_REQUIRED - assert sut.possible_values == POSSIBLE_VALUES + # assert sut.possible_values == POSSIBLE_VALUES assert sut.default == default ui = sut.ui From 5633487eb3f011fc5477627457afce906000dece Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 27 Dec 2023 09:46:27 +0300 Subject: [PATCH 25/56] Add additional tests --- pyatlan/pkg/models.py | 6 ++-- pyatlan/pkg/ui.py | 8 +++-- tests/unit/pkg/test_models.py | 60 ++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 30320d405..e02e5a1f8 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Literal, Optional -from pydantic import BaseModel, Field, PrivateAttr, validate_arguments +from pydantic import BaseModel, Field, PrivateAttr, StrictStr, validate_arguments from pyatlan.model.enums import AtlanConnectorType from pyatlan.pkg.ui import UIConfig @@ -16,8 +16,8 @@ class PackageConfig(BaseModel): - labels: dict[str, str] - annotations: dict[str, str] + labels: dict[StrictStr, StrictStr] + annotations: dict[StrictStr, StrictStr] class _PackageDefinition(BaseModel): diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py index 6c542bd7b..3a78c86ae 100644 --- a/pyatlan/pkg/ui.py +++ b/pyatlan/pkg/ui.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass, field -from typing import Any, Union +from typing import Any, Optional, Union from pydantic import StrictStr, validate_arguments @@ -93,11 +93,13 @@ def __init__( @dataclass() class UIConfig: steps: list[UIStep] - rules: list[Any] + rules: list[Any] = field(default_factory=list) properties: dict[str, UIElement] = field(default_factory=dict) @validate_arguments() - def __init__(self, steps: list[UIStep], rules: list[Any]): + def __init__(self, steps: list[UIStep], rules: Optional[list[Any]] = None): + if rules is None: + rules = [] self.steps = steps self.rules = rules self.properties = {} diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py index 3fb7f312d..9214a736b 100644 --- a/tests/unit/pkg/test_models.py +++ b/tests/unit/pkg/test_models.py @@ -1,4 +1,7 @@ -from pyatlan.pkg.models import NamePathS3Tuple +import pytest +from pydantic import ValidationError + +from pyatlan.pkg.models import NamePathS3Tuple, PackageConfig LABEL = "Some label" HELP = "some help" @@ -7,6 +10,61 @@ DESCRIPTION = "some description" +@pytest.fixture() +def labels(): + return {"first": "one"} + + +@pytest.fixture() +def annotations(): + return {"first": "one"} + + +@pytest.fixture() +def good_or_bad_labels(request, labels): + if request.param == "good": + return labels + else: + return {1: 1} + + +@pytest.fixture() +def good_or_bad_annotations(request, annotations): + if request.param == "good": + return annotations + else: + return {1: 1} + + +class TestPackageConfig: + @pytest.mark.parametrize( + "good_or_bad_labels, good_or_bad_annotations, msg", + [ + ( + "good", + "bad", + r"1 validation error for PackageConfig\nannotations -> __key__\n str type expected", + ), + ( + "bad", + "good", + r"1 validation error for PackageConfig\nlabels -> __key__\n str type expected", + ), + ], + indirect=["good_or_bad_labels", "good_or_bad_annotations"], + ) + def test_validation(self, good_or_bad_labels, good_or_bad_annotations, msg): + with pytest.raises(ValidationError, match=msg): + PackageConfig( + labels=good_or_bad_labels, annotations=good_or_bad_annotations + ) + + def test_constructor(self, labels, annotations): + sut = PackageConfig(labels=labels, annotations=annotations) + assert sut.labels == labels + assert sut.annotations == annotations + + class TestNamePathS3Tuple: def test_constructor(self): sut = NamePathS3Tuple(input_name="something") From c7706b8c17325e6522358b87bfa4dcf630c35eff Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 28 Dec 2023 09:55:33 +0300 Subject: [PATCH 26/56] Fix test --- tests/unit/pkg/test_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/pkg/test_ui.py b/tests/unit/pkg/test_ui.py index fd67205f8..f810eb0fe 100644 --- a/tests/unit/pkg/test_ui.py +++ b/tests/unit/pkg/test_ui.py @@ -52,7 +52,7 @@ def good_or_bad_rule(request, ui_rule): if request.param == "good": return [ui_rule] else: - return None + return 1 class TestUIConfig: @@ -70,7 +70,7 @@ def test_constructor(self, ui_step, ui_rule): ( "good", "bad", - r"1 validation error for Init\nrules\n none is not an allowed value", + r"1 validation error for Init\nrules\n value is not a valid list", ), ( "bad", From 0924aa1a0c8b69841729a2218113d5338c70ef42 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 28 Dec 2023 14:16:54 +0300 Subject: [PATCH 27/56] Additional tests --- pyatlan/pkg/models.py | 53 ++++++++ .../pkg/templates/default_configmap.jinja2 | 20 +++ tests/unit/pkg/test_models.py | 117 +++++++++++++++++- 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 pyatlan/pkg/templates/default_configmap.jinja2 diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index e02e5a1f8..67e44d629 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Literal, Optional +from jinja2 import Environment, PackageLoader from pydantic import BaseModel, Field, PrivateAttr, StrictStr, validate_arguments from pyatlan.model.enums import AtlanConnectorType @@ -217,3 +218,55 @@ def create_package(pkg: "CustomPackage", args: list[str]): config_maps_dir.mkdir(parents=True, exist_ok=True) templates_dir = root_dir / "templates" templates_dir.mkdir(parents=True, exist_ok=True) + + +class PackageWriter(BaseModel): + path: Path + pkg: CustomPackage + _root_dir: Path = PrivateAttr() + _config_maps_dir: Path = PrivateAttr() + _templates_dir: Path = PrivateAttr() + _env: Environment = PrivateAttr() + + def __init__(self, **data): + super().__init__(**data) + self._root_dir = self.path / self.pkg.name + self._config_maps_dir = self._root_dir / "configmaps" + self._templates_dir = self._root_dir / "templates" + self._env = Environment( # noqa: S701 + loader=PackageLoader("pyatlan.pkg", "templates") + ) + + def create_package(self): + self._root_dir.mkdir(parents=True, exist_ok=True) + with (self._root_dir / "index.js").open("w") as index: + index.write(CustomPackage.indexJS()) + with (self._root_dir / "index.js").open("w") as index: + index.write(CustomPackage.indexJS()) + with (self._root_dir / "package.json").open("w") as package: + package.write(self.pkg.packageJSON) + self.create_templates() + self.create_configmaps() + + def create_templates(self): + self._templates_dir.mkdir(parents=True, exist_ok=True) + template = self._env.get_template("default_template.jinja2") + content = template.render({"pkg": self.pkg}) + with (self._templates_dir / "default.yaml").open("w") as script: + script.write(content) + + def create_configmaps(self): + self._config_maps_dir.mkdir(parents=True, exist_ok=True) + template = self._env.get_template("default_configmap.jinja2") + content = template.render({"pkg": self.pkg}) + with (self._config_maps_dir / "default.yaml").open("w") as script: + script.write(content) + + +@validate_arguments() +def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "config"]): + writer = PackageWriter(pkg=pkg, path=path) + if operation == "package": + writer.create_package() + else: + writer.create_config() diff --git a/pyatlan/pkg/templates/default_configmap.jinja2 b/pyatlan/pkg/templates/default_configmap.jinja2 new file mode 100644 index 000000000..fcfb35926 --- /dev/null +++ b/pyatlan/pkg/templates/default_configmap.jinja2 @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ pkg.name }} +data: + config: |- + { + "properties": { +{%- for key, value in pkg.ui_config.properties.items() %} + "{{ key }}": { + "type": {{ value.type_ }} + "required": {{ value.required | tojson}} + "ui": { + } + } +{% endfor %} + + } + + } diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py index 9214a736b..07e723ac5 100644 --- a/tests/unit/pkg/test_models.py +++ b/tests/unit/pkg/test_models.py @@ -1,7 +1,18 @@ +from pathlib import Path +from unittest.mock import patch + import pytest from pydantic import ValidationError -from pyatlan.pkg.models import NamePathS3Tuple, PackageConfig +from pyatlan.pkg.models import ( + CustomPackage, + NamePathS3Tuple, + PackageConfig, + PackageWriter, + generate, +) +from pyatlan.pkg.ui import UIConfig, UIStep +from pyatlan.pkg.widgets import TextInput LABEL = "Some label" HELP = "some help" @@ -36,6 +47,46 @@ def good_or_bad_annotations(request, annotations): return {1: 1} +@pytest.fixture() +def mock_package_writer(): + with patch("pyatlan.pkg.models.PackageWriter") as package_writer: + yield package_writer.return_value + + +@pytest.fixture() +def custom_package(): + text_input = TextInput( + label="Qualified name prefix", + help_="Provide the starting name for schemas from which to propagate ownership", + required=False, + placeholder="default/snowflake/1234567890", + grid=4, + ) + ui_step = UIStep( + title="Configuration", + description="Owner propagation configuration", + inputs={"qn_prefix": text_input}, + ) + return CustomPackage( + package_id="@csa/owner-propagator", + package_name="Owner Propagator", + description="Propagate owners from schema downwards.", + docs_url="https://solutions.atlan.com/", + icon_url="https://assets.atlan.com/assets/ph-user-switch-light.svg", + container_image="ghcr.io/atlanhq/csa-owner-propagator:1", + container_command=["doit"], + ui_config=UIConfig(steps=[ui_step]), + ) + + +@pytest.fixture() +def good_or_bad_custom_package(request, custom_package): + if request.param == "good": + return custom_package + else: + return None + + class TestPackageConfig: @pytest.mark.parametrize( "good_or_bad_labels, good_or_bad_annotations, msg", @@ -83,3 +134,67 @@ def test_constructor(self, ui_step): # inputs={"assets_file": file_uploader}) # ]) # sut = WorkflowInputs(config=config, pkg_name="bob")bob + + +class TestPackageWriter: + def test_constructor(self, custom_package, tmp_path): + sut = PackageWriter(pkg=custom_package, path=tmp_path) + + assert sut.path == tmp_path + assert sut.pkg == custom_package + + def test_create_package(self, custom_package, tmp_path: Path): + sut = PackageWriter(pkg=custom_package, path=tmp_path) + + sut.create_package() + root_dir = tmp_path / custom_package.name + assert root_dir.exists() + assert (root_dir / "index.js").exists() + assert (root_dir / "package.json").exists() + configmaps = root_dir / "configmaps" + assert configmaps.exists() + assert (configmaps / "default.yaml").exists() + assert (root_dir / "templates").exists() + + +@pytest.mark.parametrize( + "good_or_bad_custom_package, path, operation, msg", + [ + ( + "bad", + ".", + "package", + r"1 validation error for Generate\npkg\n none is not an allowed value", + ), + ( + "good", + 1, + "config", + r"1 validation error for Generate\npath\n value is not a valid path", + ), + ( + "good", + ".", + "bad", + r"1 validation error for Generate\noperation\n unexpected value; permitted: 'package', 'config'", + ), + ], + indirect=["good_or_bad_custom_package"], +) +def test_generate_parameter_validation( + good_or_bad_custom_package, path, operation, msg +): + with pytest.raises(ValidationError, match=msg): + generate(pkg=good_or_bad_custom_package, path=path, operation=operation) + + +def test_generate_with_package(mock_package_writer, custom_package): + generate(pkg=custom_package, path="..", operation="package") + + mock_package_writer.create_package.assert_called() + + +def test_generate_with_config(mock_package_writer, custom_package): + generate(pkg=custom_package, path="..", operation="config") + + mock_package_writer.create_config.assert_called() From f976775a2c3eb068030864f2e5ae1330d16875eb Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 3 Jan 2024 08:57:52 +0300 Subject: [PATCH 28/56] Add additional tests --- pyatlan/pkg/models.py | 2 +- .../pkg/templates/default_configmap.jinja2 | 19 +- pyatlan/pkg/templates/default_template.jinja2 | 11 + pyatlan/pkg/ui.py | 24 +- pyatlan/pkg/widgets.py | 226 +++++++++--------- pyatlan/utils.py | 2 +- tests/integration/custom_package_test.py | 2 +- tests/unit/pkg/conftest.py | 2 +- tests/unit/pkg/test_models.py | 8 +- tests/unit/pkg/test_widgets.py | 170 ++++++------- 10 files changed, 255 insertions(+), 211 deletions(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 67e44d629..5d7f09177 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -269,4 +269,4 @@ def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "conf if operation == "package": writer.create_package() else: - writer.create_config() + writer.create_configmaps() diff --git a/pyatlan/pkg/templates/default_configmap.jinja2 b/pyatlan/pkg/templates/default_configmap.jinja2 index fcfb35926..c99877e06 100644 --- a/pyatlan/pkg/templates/default_configmap.jinja2 +++ b/pyatlan/pkg/templates/default_configmap.jinja2 @@ -10,11 +10,18 @@ data: "{{ key }}": { "type": {{ value.type_ }} "required": {{ value.required | tojson}} - "ui": { - } + "ui": {{ value.ui.to_json() | indent(10)}} } -{% endfor %} - - } - +{%- endfor %} + }, + "anyOf": [ +{%- for rule in pkg.ui_config.rules %} + {{ rule.to_json() | indent(10) }} +{%- endfor %} + ], + "steps": [ +{%- for step in pkg.ui_config.steps %} + {{ step.to_json() | indent(10) }} +{%- endfor %} + ] } diff --git a/pyatlan/pkg/templates/default_template.jinja2 b/pyatlan/pkg/templates/default_template.jinja2 index c0886f648..8016b6b0b 100644 --- a/pyatlan/pkg/templates/default_template.jinja2 +++ b/pyatlan/pkg/templates/default_template.jinja2 @@ -46,3 +46,14 @@ spec: secretKeyRef: name: argo-client-creds key: password +{%- for name, property in pkg.ui_config.properties.items() %} + - name: {{ name | upper }} + value: "{% raw %}{{inputs.parameters.{% endraw %}{{name}}{% raw %}}}{% endraw %}" +{%- endfor %} + - name: NESTED_CONFIG + value: |- + { +{%- for name, property in pkg.ui_config.properties.items() %} + "{{ name }}": {% raw %}{{=toJson(inputs.parameters.{% endraw %}{{ name}}{% raw %})}}{% endraw %}, +{%- endfor %} + } diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py index 3a78c86ae..e1b537b30 100644 --- a/pyatlan/pkg/ui.py +++ b/pyatlan/pkg/ui.py @@ -1,8 +1,10 @@ +import json import logging from dataclasses import dataclass, field -from typing import Any, Optional, Union +from typing import Any, Optional, TypeVar, Union from pydantic import StrictStr, validate_arguments +from pydantic.json import pydantic_encoder from pyatlan.pkg.widgets import ( APITokenSelector, @@ -44,6 +46,8 @@ TextInput, ] +TUIStep = TypeVar("TUIStep", bound="UIStep") + @dataclass() class UIStep: @@ -66,6 +70,22 @@ def __init__( self.id = title.replace(" ", "_").lower() self.properties = list(self.inputs.keys()) + def to_json(self: TUIStep) -> str: + @dataclass() + class Inner: + title: str + description: str = "" + id: str = "" + properties: list[str] = field(default_factory=list) + + inner = Inner( + title=self.title, + description=self.description, + id=self.id, + properties=self.properties, + ) + return json.dumps(inner, indent=2, default=pydantic_encoder) + @dataclass() class UIRule: @@ -106,5 +126,5 @@ def __init__(self, steps: list[UIStep], rules: Optional[list[Any]] = None): for step in steps: for key, value in step.inputs.items(): if key in self.properties: - LOGGER.warning("Duplicate key found accross steps: %s", key) + LOGGER.warning("Duplicate key found across steps: %s", key) self.properties[key] = value diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 94affb0be..63ec662b0 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -1,12 +1,21 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2023 Atlan Pte. Ltd. import abc +import json from dataclasses import dataclass, field # from dataclasses import dataclass from typing import Optional, Union -from pydantic import StrictBool, StrictInt, StrictStr, validate_arguments +from pydantic import ( + Field, + StrictBool, + StrictInt, + StrictStr, + dataclasses, + validate_arguments, +) +from pydantic.json import pydantic_encoder # from pydantic.dataclasses import dataclass @@ -31,15 +40,18 @@ ] -@dataclass +@dataclasses.dataclass class AbstractWidget(abc.ABC): widget: str label: str hidden: bool = False - help_: str = "" + help: str = Field(default="") placeholder: str = "" grid: int = 8 + def to_json(self): + return json.dumps(self, indent=2, default=pydantic_encoder) + @dataclass class AbstractUIElement(abc.ABC): @@ -66,20 +78,20 @@ def __init__( self.enum_names = list(possible_values.values()) -@dataclass +@dataclasses.dataclass class APITokenSelectorWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", grid: int = 4, ): super().__init__( widget="apiTokenSelect", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -92,7 +104,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 4, ): """ @@ -108,18 +120,18 @@ def __init__( :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = APITokenSelectorWidget( - label=label, hidden=hidden, help_=help_, grid=grid + label=label, hidden=hidden, help=help, grid=grid ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class BooleanInputWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", placeholder: str = "", grid: int = 8, ): @@ -127,7 +139,7 @@ def __init__( widget="boolean", label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, ) @@ -141,7 +153,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -150,23 +162,23 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ - widget = BooleanInputWidget(label=label, hidden=hidden, help_=help_, grid=grid) + widget = BooleanInputWidget(label=label, hidden=hidden, help=help, grid=grid) super().__init__(type_="boolean", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class ConnectionCreatorWidget(AbstractWidget): def __init__( - self, label: str, hidden: bool = False, help_: str = "", placeholder: str = "" + self, label: str, hidden: bool = False, help: str = "", placeholder: str = "" ): super().__init__( widget="connection", label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, ) @@ -179,7 +191,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", placeholder: StrictStr = "", ): """ @@ -189,16 +201,16 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param placeholder: example text to place within the widget to exemplify its use """ widget = ConnectionCreatorWidget( - label=label, hidden=hidden, help_=help_, placeholder=placeholder + label=label, hidden=hidden, help=help, placeholder=placeholder ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class ConnectionSelectorWidget(AbstractWidget): start: int = 1 @@ -206,7 +218,7 @@ def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", placeholder: str = "", grid: int = 4, start: int = 1, @@ -215,7 +227,7 @@ def __init__( widget="connectionSelector", label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, ) @@ -232,7 +244,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", placeholder: StrictStr = "", grid: StrictInt = 4, start: StrictInt = 1, @@ -244,7 +256,7 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param placeholder: example text to place within the widget to exemplify its use :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) :param start: TBC @@ -252,7 +264,7 @@ def __init__( widget = ConnectionSelectorWidget( label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, start=start, @@ -261,7 +273,7 @@ def __init__( self.start = start -@dataclass +@dataclasses.dataclass class ConnectorTypeSelectorWidget(AbstractWidget): start: int = 1 @@ -269,7 +281,7 @@ def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", grid: int = 4, start: int = 1, ): @@ -277,7 +289,7 @@ def __init__( widget="sourceConnectionSelector", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) self.start = start @@ -291,7 +303,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 4, start: StrictInt = 1, ): @@ -304,21 +316,21 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) :aram start: TBC """ widget = ConnectorTypeSelectorWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, start=start, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class DateInputWidget(AbstractWidget): min_: int = -14 max_: int = 0 @@ -329,7 +341,7 @@ def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", min_: int = -14, max_: int = 0, default: int = 0, @@ -337,7 +349,7 @@ def __init__( grid: int = 4, ): super().__init__( - widget="date", label=label, hidden=hidden, help_=help_, grid=grid + widget="date", label=label, hidden=hidden, help=help, grid=grid ) self.start = start self.max_ = max_ @@ -353,7 +365,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", min_: StrictInt = -14, max_: StrictInt = 0, default: StrictInt = 0, @@ -367,7 +379,7 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param min_: an offset from today (0) that indicates how far back in the calendar can be selected (-1 is yesterday, 1 is tomorrow, and so on) :param max_: an offset from today (0) that indicates how far forward in the calendar can be selected @@ -380,7 +392,7 @@ def __init__( widget = DateInputWidget( label=label, hidden=hidden, - help_=help_, + help=help, min_=min_, max_=max_, default=default, @@ -390,7 +402,7 @@ def __init__( super().__init__(type_="number", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class DropDownWidget(AbstractWidget): mode: str = "" @@ -399,14 +411,14 @@ def __init__( label: str, mode: str, hidden: bool = False, - help_: str = "", + help: str = "", grid: int = 8, ): super().__init__( widget="select", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) self.mode = mode @@ -423,7 +435,7 @@ def __init__( possible_values: dict[str, str], required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", multi_select: StrictBool = False, grid: StrictInt = 8, ): @@ -434,7 +446,7 @@ def __init__( :param possible_values: map of option keys to the value that will be display for each option in the drop-down :param required: whether a value must be selected to proceed with the UI setup :param hidden whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param multi_select: whether multiple options can be selected (true) or only a single option (false) :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ @@ -442,7 +454,7 @@ def __init__( label=label, mode="multiple" if multi_select else "", hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__( @@ -454,7 +466,7 @@ def __init__( self.possible_values = possible_values -@dataclass +@dataclasses.dataclass class FileUploaderWidget(AbstractWidget): file_types: list[str] = field(default_factory=list) @@ -463,14 +475,14 @@ def __init__( label: str, file_types: list[str], hidden: bool = False, - help_: str = "", + help: str = "", placeholder: str = "", ): super().__init__( widget="fileUpload", label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, ) self.file_types = file_types @@ -485,7 +497,7 @@ def __init__( file_types: list[str], required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", placeholder: StrictStr = "", ): """ @@ -496,29 +508,27 @@ def __init__( :param file_types: list of the mime-types of files that should be accepted :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param placeholder: placeholder example text to place within the widget to exemplify its use """ widget = FileUploaderWidget( label=label, file_types=file_types, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class KeygenInputWidget(AbstractWidget): - def __init__( - self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 - ): + def __init__(self, label: str, hidden: bool = False, help: str = "", grid: int = 8): super().__init__( widget="keygen", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -531,7 +541,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -542,28 +552,26 @@ def __init__( :param label: name to show in the UI for the widge :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = KeygenInputWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class MultipleGroupsWidget(AbstractWidget): - def __init__( - self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 - ): + def __init__(self, label: str, hidden: bool = False, help: str = "", grid: int = 8): super().__init__( widget="groupMultiple", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -576,7 +584,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -585,28 +593,26 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = MultipleGroupsWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class MultipleUsersWidget(AbstractWidget): - def __init__( - self, label: str, hidden: bool = False, help_: str = "", grid: int = 8 - ): + def __init__(self, label: str, hidden: bool = False, help: str = "", grid: int = 8): super().__init__( widget="groupMultiple", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -619,7 +625,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -628,25 +634,25 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = MultipleUsersWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class NumericInputWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", placeholder: str = "", grid: int = 8, ): @@ -654,7 +660,7 @@ def __init__( widget="inputNumber", label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, ) @@ -668,7 +674,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", placeholder: StrictStr = "", grid: StrictInt = 8, ): @@ -679,34 +685,34 @@ def __init__( :param label name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param placeholder: example text to place within the widget to exemplify its use :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = NumericInputWidget( label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, ) super().__init__(type_="number", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class PasswordInputWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", grid: int = 8, ): super().__init__( widget="password", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -719,7 +725,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -729,31 +735,31 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = PasswordInputWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class RadioWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", ): super().__init__( widget="radio", label=label, hidden=hidden, - help_=help_, + help=help, ) @@ -767,7 +773,7 @@ def __init__( default: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", ): """ Widget that allows you to select just one option from a set of options, and returns the key of the selected @@ -778,13 +784,13 @@ def __init__( :param default: the default value to select in the UI, given as the string key of the option :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input """ widget = RadioWidget( label=label, hidden=hidden, - help_=help_, + help=help, ) super().__init__( type_="string", @@ -795,20 +801,20 @@ def __init__( self.default = default -@dataclass +@dataclasses.dataclass class SingleGroupWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", grid: int = 8, ): super().__init__( widget="groups", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -821,7 +827,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -830,32 +836,32 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = SingleGroupWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class SingleUserWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", grid: int = 8, ): super().__init__( widget="users", label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) @@ -868,7 +874,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", grid: StrictInt = 8, ): """ @@ -877,25 +883,25 @@ def __init__( :param label: name to show in the UI for the widget :param required: whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) """ widget = SingleUserWidget( label=label, hidden=hidden, - help_=help_, + help=help, grid=grid, ) super().__init__(type_="string", required=required, ui=widget) -@dataclass +@dataclasses.dataclass class TextInputWidget(AbstractWidget): def __init__( self, label: str, hidden: bool = False, - help_: str = "", + help: str = "", placeholder: str = "", grid: int = 8, ): @@ -903,7 +909,7 @@ def __init__( widget="input", label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, ) @@ -917,7 +923,7 @@ def __init__( label: StrictStr, required: StrictBool = False, hidden: StrictBool = False, - help_: StrictStr = "", + help: StrictStr = "", placeholder: StrictStr = "", grid: StrictInt = 8, ): @@ -928,7 +934,7 @@ def __init__( :param label: name to show in the UI for the widget :param required" whether a value must be selected to proceed with the UI setup :param hidden: whether the widget will be shown in the UI (false) or not (true) - :param help_: informational text to place in a hover-over to describe the use of the input + :param help: informational text to place in a hover-over to describe the use of the input :param placeholder: example text to place within the widget to exemplify its use :param grid: sizing of the input on the UI (8 is full-width, 4 is half-width) @@ -936,7 +942,7 @@ def __init__( widget = TextInputWidget( label=label, hidden=hidden, - help_=help_, + help=help, placeholder=placeholder, grid=grid, ) diff --git a/pyatlan/utils.py b/pyatlan/utils.py index 918c03fb6..5d80d5d21 100644 --- a/pyatlan/utils.py +++ b/pyatlan/utils.py @@ -36,7 +36,7 @@ def to_camel_case(s: str) -> str: return "".join([s[0].lower(), s[1:]]) -def get_epoch_timestamp() -> str: +def get_epoch_timestamp() -> float: return datetime.now().timestamp() diff --git a/tests/integration/custom_package_test.py b/tests/integration/custom_package_test.py index 66e8414ae..dbfbe2517 100644 --- a/tests/integration/custom_package_test.py +++ b/tests/integration/custom_package_test.py @@ -19,7 +19,7 @@ def test_custom_package(): inputs={ "qn_prefix": TextInput( label="Qualified name prefix", - help_="Provide the starting name for schemas from which to propagate ownership", + help="Provide the starting name for schemas from which to propagate ownership", required=False, placeholder="default/snowflake/1234567890", grid=4, diff --git a/tests/unit/pkg/conftest.py b/tests/unit/pkg/conftest.py index a618f8aaa..00a0a82c1 100644 --- a/tests/unit/pkg/conftest.py +++ b/tests/unit/pkg/conftest.py @@ -64,7 +64,7 @@ def file_uploader() -> FileUploader: label=LABEL, file_types=["text/csv"], required=False, - help_=HELP, + help=HELP, placeholder=PLACEHOLDER, ) return uploader diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py index 07e723ac5..01422f434 100644 --- a/tests/unit/pkg/test_models.py +++ b/tests/unit/pkg/test_models.py @@ -57,7 +57,7 @@ def mock_package_writer(): def custom_package(): text_input = TextInput( label="Qualified name prefix", - help_="Provide the starting name for schemas from which to propagate ownership", + help="Provide the starting name for schemas from which to propagate ownership", required=False, placeholder="default/snowflake/1234567890", grid=4, @@ -188,13 +188,13 @@ def test_generate_parameter_validation( generate(pkg=good_or_bad_custom_package, path=path, operation=operation) -def test_generate_with_package(mock_package_writer, custom_package): +def test_generate_with_operation_package(mock_package_writer, custom_package): generate(pkg=custom_package, path="..", operation="package") mock_package_writer.create_package.assert_called() -def test_generate_with_config(mock_package_writer, custom_package): +def test_generate_with_operation_config(mock_package_writer, custom_package): generate(pkg=custom_package, path="..", operation="config") - mock_package_writer.create_config.assert_called() + mock_package_writer.create_configmaps.assert_called() diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 6cc90edf5..5182b636b 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -60,7 +60,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "apiTokenSelect" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 4 def test_constructor_with_overrides(self): @@ -68,7 +68,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 1), ) assert sut.type_ == "string" @@ -80,7 +80,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "apiTokenSelect" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -124,7 +124,7 @@ def test_constructor_with_overrides(self): True, 1, 1, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -139,7 +139,7 @@ def test_constructor_with_overrides(self): def test_validation(self, label, required, hidden, help_, grid, msg): with pytest.raises(ValidationError, match=msg): APITokenSelector( - label=label, required=required, hidden=hidden, help_=help_, grid=grid + label=label, required=required, hidden=hidden, help=help_, grid=grid ) @@ -155,7 +155,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "boolean" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -163,7 +163,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "boolean" @@ -175,7 +175,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "boolean" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -219,7 +219,7 @@ def test_constructor_with_overrides(self): True, 1, 1, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -234,7 +234,7 @@ def test_constructor_with_overrides(self): def test_validation(self, label, required, hidden, help_, grid, msg): with pytest.raises(ValidationError, match=msg): BooleanInput( - label=label, required=required, hidden=hidden, help_=help_, grid=grid + label=label, required=required, hidden=hidden, help=help_, grid=grid ) @@ -250,7 +250,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "connection" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.placeholder == "" def test_constructor_with_overrides(self): @@ -258,7 +258,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, placeholder=PLACE_HOLDER, ) assert sut.type_ == "string" @@ -270,7 +270,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "connection" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.placeholder == PLACE_HOLDER @pytest.mark.parametrize( @@ -314,7 +314,7 @@ def test_constructor_with_overrides(self): True, 1, PLACE_HOLDER, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -332,7 +332,7 @@ def test_validation(self, label, required, hidden, help_, placeholder, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, placeholder=placeholder, ) @@ -349,7 +349,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "connectionSelector" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.placeholder == "" assert ui.grid == 4 assert ui.start == 1 @@ -359,7 +359,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, placeholder=PLACE_HOLDER, grid=(grid := 2), start=(start := 10), @@ -373,7 +373,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "connectionSelector" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.placeholder == PLACE_HOLDER assert ui.grid == grid assert ui.start == start @@ -429,7 +429,7 @@ def test_constructor_with_overrides(self): PLACE_HOLDER, 1, 2, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -471,7 +471,7 @@ def test_validation( label=label, required=required, hidden=hidden, - help_=help_, + help=help_, placeholder=placeholder, grid=grid, start=start, @@ -490,7 +490,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "sourceConnectionSelector" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 4 assert ui.start == 1 @@ -499,7 +499,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 2), start=(start := 10), ) @@ -512,7 +512,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "sourceConnectionSelector" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid assert ui.start == start @@ -562,7 +562,7 @@ def test_constructor_with_overrides(self): 1, 1, 2, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -590,7 +590,7 @@ def test_validation(self, label, required, hidden, help_, grid, start, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, start=start, ) @@ -608,7 +608,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "date" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.min_ == -14 assert ui.max_ == 0 assert ui.default == 0 @@ -620,7 +620,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, min_=(min_ := -2), max_=(max_ := 3), default=(default := 1), @@ -636,7 +636,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "date" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.min_ == min_ assert ui.max_ == max_ assert ui.default == default @@ -692,7 +692,7 @@ def test_constructor_with_overrides(self): 1, 0, 4, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -764,7 +764,7 @@ def test_validation( label=label, required=required, hidden=hidden, - help_=help_, + help=help_, min_=min_, max_=max_, default=default, @@ -787,7 +787,7 @@ def test_constructor_with_defaults(self): assert ui.label == LABEL assert ui.mode == "" assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -796,7 +796,7 @@ def test_constructor_with_overrides(self): possible_values=POSSIBLE_VALUES, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, multi_select=True, grid=(grid := 2), ) @@ -810,7 +810,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "select" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.mode == "multiple" assert ui.grid == grid @@ -865,7 +865,7 @@ def test_constructor_with_overrides(self): 1, False, 4, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -898,7 +898,7 @@ def test_validation( possible_values=possible_values, required=required, hidden=hidden, - help_=help_, + help=help_, multi_select=multi_select, grid=grid, ) @@ -917,7 +917,7 @@ def test_constructor_with_defaults(self): assert ui.label == LABEL assert ui.file_types == FILE_TYPES assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.placeholder == "" def test_constructor_with_overrides(self): @@ -926,7 +926,7 @@ def test_constructor_with_overrides(self): file_types=FILE_TYPES, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, placeholder=PLACE_HOLDER, ) assert sut.type_ == "string" @@ -939,7 +939,7 @@ def test_constructor_with_overrides(self): assert ui.label == LABEL assert ui.file_types == FILE_TYPES assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.placeholder == PLACE_HOLDER @pytest.mark.parametrize( @@ -988,7 +988,7 @@ def test_constructor_with_overrides(self): True, 1, PLACE_HOLDER, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1010,7 +1010,7 @@ def test_validation( file_types=file_types, required=required, hidden=hidden, - help_=help_, + help=help_, placeholder=placeholder, ) @@ -1029,7 +1029,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "keygen" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -1037,7 +1037,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "string" @@ -1049,7 +1049,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "keygen" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -1085,7 +1085,7 @@ def test_constructor_with_overrides(self): True, 1, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1103,7 +1103,7 @@ def test_validation(self, label, required, hidden, help_, grid, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, ) @@ -1122,7 +1122,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "groupMultiple" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -1130,7 +1130,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "string" @@ -1142,7 +1142,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "groupMultiple" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -1178,7 +1178,7 @@ def test_constructor_with_overrides(self): True, 1, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1196,7 +1196,7 @@ def test_validation(self, label, required, hidden, help_, grid, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, ) @@ -1215,7 +1215,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "groupMultiple" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -1223,7 +1223,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "string" @@ -1235,7 +1235,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "groupMultiple" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -1271,7 +1271,7 @@ def test_constructor_with_overrides(self): True, 1, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1289,7 +1289,7 @@ def test_validation(self, label, required, hidden, help_, grid, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, ) @@ -1308,7 +1308,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "inputNumber" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.placeholder == "" assert ui.grid == 8 @@ -1317,7 +1317,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, placeholder=PLACE_HOLDER, grid=(grid := 3), ) @@ -1330,7 +1330,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "inputNumber" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.placeholder == PLACE_HOLDER assert ui.grid == grid @@ -1371,7 +1371,7 @@ def test_constructor_with_overrides(self): 1, PLACE_HOLDER, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1399,7 +1399,7 @@ def test_validation(self, label, required, hidden, help_, placeholder, grid, msg label=label, required=required, hidden=hidden, - help_=help_, + help=help_, placeholder=placeholder, grid=grid, ) @@ -1419,7 +1419,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "password" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -1427,7 +1427,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "string" @@ -1439,7 +1439,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "password" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -1475,7 +1475,7 @@ def test_constructor_with_overrides(self): True, 1, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1493,7 +1493,7 @@ def test_validation(self, label, required, hidden, help_, grid, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, ) @@ -1514,7 +1514,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "radio" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" def test_constructor_with_overrides(self): sut = Radio( @@ -1523,7 +1523,7 @@ def test_constructor_with_overrides(self): default=(default := "a"), required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, ) assert sut.type_ == "string" assert sut.required == IS_REQUIRED @@ -1536,7 +1536,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "radio" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP @pytest.mark.parametrize( "label, possible_values, default, required, hidden, help_, msg", @@ -1593,7 +1593,7 @@ def test_constructor_with_overrides(self): True, True, 1, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ], ) @@ -1607,7 +1607,7 @@ def test_validation( default=default, required=required, hidden=hidden, - help_=help_, + help=help_, ) @@ -1625,7 +1625,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "groups" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -1633,7 +1633,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "string" @@ -1645,7 +1645,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "groups" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -1681,7 +1681,7 @@ def test_constructor_with_overrides(self): True, 1, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1699,7 +1699,7 @@ def test_validation(self, label, required, hidden, help_, grid, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, ) @@ -1718,7 +1718,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "users" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.grid == 8 def test_constructor_with_overrides(self): @@ -1726,7 +1726,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, grid=(grid := 3), ) assert sut.type_ == "string" @@ -1738,7 +1738,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "users" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.grid == grid @pytest.mark.parametrize( @@ -1774,7 +1774,7 @@ def test_constructor_with_overrides(self): True, 1, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1792,7 +1792,7 @@ def test_validation(self, label, required, hidden, help_, grid, msg): label=label, required=required, hidden=hidden, - help_=help_, + help=help_, grid=grid, ) @@ -1811,7 +1811,7 @@ def test_constructor_with_defaults(self): assert ui.widget == "input" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN - assert ui.help_ == "" + assert ui.help == "" assert ui.placeholder == "" assert ui.grid == 8 @@ -1820,7 +1820,7 @@ def test_constructor_with_overrides(self): label=LABEL, required=IS_REQUIRED, hidden=IS_HIDDEN, - help_=HELP, + help=HELP, placeholder=PLACE_HOLDER, grid=(grid := 3), ) @@ -1833,7 +1833,7 @@ def test_constructor_with_overrides(self): assert ui.widget == "input" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN - assert ui.help_ == HELP + assert ui.help == HELP assert ui.placeholder == PLACE_HOLDER assert ui.grid == grid @@ -1874,7 +1874,7 @@ def test_constructor_with_overrides(self): 1, PLACE_HOLDER, 3, - r"1 validation error for Init\nhelp_\n str type expected", + r"1 validation error for Init\nhelp\n str type expected", ), ( LABEL, @@ -1902,7 +1902,7 @@ def test_validation(self, label, required, hidden, help_, placeholder, grid, msg label=label, required=required, hidden=hidden, - help_=help_, + help=help_, placeholder=placeholder, grid=grid, ) From ff8f256002bd8fe18432293f315e737e30c8510b Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 3 Jan 2024 14:40:44 +0300 Subject: [PATCH 29/56] More testing --- pyatlan/pkg/models.py | 2 + pyatlan/pkg/templates/default_template.jinja2 | 12 +++++- pyatlan/pkg/widgets.py | 40 +++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 5d7f09177..558a9b41a 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -166,6 +166,7 @@ class CustomPackage(BaseModel): preview: bool = False connector_type: Optional[AtlanConnectorType] = None category: str = "custom" + outputs: dict[str, str] = Field(default_factory=dict) _pkg: PackageDefinition = PrivateAttr() _name: str = PrivateAttr() @@ -236,6 +237,7 @@ def __init__(self, **data): self._env = Environment( # noqa: S701 loader=PackageLoader("pyatlan.pkg", "templates") ) + self._env.globals.update({"isinstanceof": isinstance}) def create_package(self): self._root_dir.mkdir(parents=True, exist_ok=True) diff --git a/pyatlan/pkg/templates/default_template.jinja2 b/pyatlan/pkg/templates/default_template.jinja2 index 8016b6b0b..660dc44f8 100644 --- a/pyatlan/pkg/templates/default_template.jinja2 +++ b/pyatlan/pkg/templates/default_template.jinja2 @@ -10,10 +10,18 @@ spec: {%- if pkg.name %} - name: output_prefix value: {{ pkg.name }} +{%- for name, property in pkg.ui_config.properties.items() %} + - name: {{ name }} + value: {{ property.ui.parameter_value}} +{%- endfor %} {%- endif %} - artifacts: + artifacts: [] outputs: artifacts: +{%- for name, value in pkg.outputs.items() %} + - name: {{ name }} + path: {{ value }} +{%- endfor %} container: image: {{ pkg.container_image }} imagePullPolicy: {{ pkg.container_image_pull_policy.value }} @@ -54,6 +62,6 @@ spec: value: |- { {%- for name, property in pkg.ui_config.properties.items() %} - "{{ name }}": {% raw %}{{=toJson(inputs.parameters.{% endraw %}{{ name}}{% raw %})}}{% endraw %}, + "{{ name }}": {{ property.ui.to_nested(name) }}{% if not loop.last %},{% endif %} {%- endfor %} } diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 63ec662b0..b13d376f8 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -52,6 +52,13 @@ class AbstractWidget(abc.ABC): def to_json(self): return json.dumps(self, indent=2, default=pydantic_encoder) + def to_nested(self, name: str) -> str: + return f"{{{{=toJson(inputs.parameters.{name})}}}}" + + @property + def parameter_value(self) -> str: + return '""' + @dataclass class AbstractUIElement(abc.ABC): @@ -65,6 +72,7 @@ class UIElementWithEnum(AbstractUIElement): enum: list[str] enum_names: list[str] default: Optional[str] = None + possible_values: dict[str, str] = field(default_factory=dict) def __init__( self, @@ -76,6 +84,7 @@ def __init__( super().__init__(type_=type_, required=required, ui=ui) self.enum = list(possible_values.keys()) self.enum_names = list(possible_values.values()) + self.possible_values = possible_values @dataclasses.dataclass @@ -144,6 +153,13 @@ def __init__( grid=grid, ) + def to_nested(self, name: str) -> str: + return f"{{{{inputs.parameters.{name}}}}}" + + @property + def parameter_value(self) -> str: + return "false" + @dataclass class BooleanInput(AbstractUIElement): @@ -356,6 +372,13 @@ def __init__( self.min_ = min_ self.default = default + def to_nested(self, name: str) -> str: + return f"{{{{inputs.parameters.{name}}}}}" + + @property + def parameter_value(self) -> str: + return "-1" + @dataclass class DateInput(AbstractUIElement): @@ -426,8 +449,6 @@ def __init__( @dataclass class DropDown(UIElementWithEnum): - possible_values: dict[str, str] = field(default_factory=dict) - @validate_arguments() def __init__( self, @@ -463,7 +484,6 @@ def __init__( possible_values=possible_values, ui=widget, ) - self.possible_values = possible_values @dataclasses.dataclass @@ -487,6 +507,13 @@ def __init__( ) self.file_types = file_types + def to_nested(self, name: str) -> str: + return f"/tmp/{name}/{{inputs.parameters.{name}}}" # noqa: S108 + + @property + def parameter_value(self) -> str: + return '"argo-artifacts/atlan-update/last-run-timestamp.txt"' + @dataclass class FileUploader(AbstractUIElement): @@ -665,6 +692,13 @@ def __init__( grid=grid, ) + def to_nested(self, name: str) -> str: + return f"{{{{inputs.parameters.{name}}}}}" + + @property + def parameter_value(self) -> str: + return "-1" + @dataclass class NumericInput(AbstractUIElement): From fb28dd7837dc67adfe36a75367407b6fe8d998a0 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 3 Jan 2024 15:07:24 +0300 Subject: [PATCH 30/56] Add missing element --- pyatlan/pkg/ui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py index e1b537b30..0c5c35462 100644 --- a/pyatlan/pkg/ui.py +++ b/pyatlan/pkg/ui.py @@ -10,6 +10,7 @@ APITokenSelector, BooleanInput, ConnectionCreator, + ConnectionSelector, ConnectorTypeSelector, DateInput, DropDown, @@ -31,6 +32,7 @@ APITokenSelector, BooleanInput, ConnectionCreator, + ConnectionSelector, ConnectorTypeSelector, DateInput, DropDown, From e63fddfc578fb7920a7b553612996d941356f420 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 3 Jan 2024 19:11:15 +0300 Subject: [PATCH 31/56] Modify output --- pyatlan/pkg/templates/default_template.jinja2 | 17 +++++++++++++++- pyatlan/pkg/widgets.py | 20 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pyatlan/pkg/templates/default_template.jinja2 b/pyatlan/pkg/templates/default_template.jinja2 index 660dc44f8..9a030a61e 100644 --- a/pyatlan/pkg/templates/default_template.jinja2 +++ b/pyatlan/pkg/templates/default_template.jinja2 @@ -15,7 +15,22 @@ spec: value: {{ property.ui.parameter_value}} {%- endfor %} {%- endif %} +{%- set ns = namespace(has_s3=false) %} +{%- for name, property in pkg.ui_config.properties.items() %} + {%- if property.ui.s3_artifact %} + {%- if loop.first %} + artifacts: + {%- endif %} + {%- set ns.has_s3 = true %} + - name: {{ name }}_s3 + path: "/tmp/{{ name }}/{% raw %}{{inputs.parameters.{% endraw %}{{ name }}{% raw %}}}{% endraw %}" + s3: + key: "{% raw %}{{inputs.parameters.{% endraw %}{{ name }}{% raw %}}}{% endraw %}" + {%- endif %} +{%- endfor %} +{%- if not ns.has_s3 %} artifacts: [] +{% endif %} outputs: artifacts: {%- for name, value in pkg.outputs.items() %} @@ -56,7 +71,7 @@ spec: key: password {%- for name, property in pkg.ui_config.properties.items() %} - name: {{ name | upper }} - value: "{% raw %}{{inputs.parameters.{% endraw %}{{name}}{% raw %}}}{% endraw %}" + value: "{{property.ui.to_env(name)}}" {%- endfor %} - name: NESTED_CONFIG value: |- diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index b13d376f8..b61601f35 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -55,10 +55,17 @@ def to_json(self): def to_nested(self, name: str) -> str: return f"{{{{=toJson(inputs.parameters.{name})}}}}" + def to_env(self, name: str) -> str: + return f"{{{{inputs.parameters.{name}}}}}" + @property def parameter_value(self) -> str: return '""' + @property + def s3_artifact(self) -> bool: + return False + @dataclass class AbstractUIElement(abc.ABC): @@ -508,12 +515,19 @@ def __init__( self.file_types = file_types def to_nested(self, name: str) -> str: - return f"/tmp/{name}/{{inputs.parameters.{name}}}" # noqa: S108 + return f'"/tmp/{name}/{{{{inputs.parameters.{name}}}}}"' # noqa: S108 @property def parameter_value(self) -> str: return '"argo-artifacts/atlan-update/last-run-timestamp.txt"' + def to_env(self, name: str) -> str: + return f"/tmp/{name}/{{{{inputs.parameters.{name}}}}}" # noqa: S108 + + @property + def s3_artifact(self) -> bool: + return True + @dataclass class FileUploader(AbstractUIElement): @@ -547,6 +561,10 @@ def __init__( ) super().__init__(type_="string", required=required, ui=widget) + @property + def parameter_value(self) -> str: + return "" + @dataclasses.dataclass class KeygenInputWidget(AbstractWidget): From e0dcb88b42761fad6ad190205eb4ea22a5efdf6c Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 3 Jan 2024 19:35:15 +0300 Subject: [PATCH 32/56] Fix broken test --- pyatlan/pkg/ui.py | 6 ++++++ tests/unit/pkg/test_ui.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py index 0c5c35462..0a567c2d5 100644 --- a/pyatlan/pkg/ui.py +++ b/pyatlan/pkg/ui.py @@ -89,6 +89,9 @@ class Inner: return json.dumps(inner, indent=2, default=pydantic_encoder) +TUIRule = TypeVar("TUIRule", bound="UIRule") + + @dataclass() class UIRule: when_inputs: dict[str, str] @@ -111,6 +114,9 @@ def __init__( self.required = required self.properties = {key: {"const": value} for key, value in when_inputs.items()} + def to_json(self: TUIRule) -> str: + return json.dumps(self.properties, indent=2, default=pydantic_encoder) + @dataclass() class UIConfig: diff --git a/tests/unit/pkg/test_ui.py b/tests/unit/pkg/test_ui.py index f810eb0fe..06039317b 100644 --- a/tests/unit/pkg/test_ui.py +++ b/tests/unit/pkg/test_ui.py @@ -176,7 +176,7 @@ def test_constructor_with_overrides(self, text_input, inputs): TITLE, {"qn_prefix": "oioi"}, "", - r"16 validation errors for Init\ninputs -> qn_prefix\n instance of APITokenSelector, tuple or dict " + r"17 validation errors for Init\ninputs -> qn_prefix\n instance of APITokenSelector, tuple or dict " r"expected", ), ( From fff0296da462d92f4dddc6a63937c21264714fa2 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Wed, 3 Jan 2024 20:30:13 +0300 Subject: [PATCH 33/56] Fix output --- pyatlan/pkg/templates/default_template.jinja2 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyatlan/pkg/templates/default_template.jinja2 b/pyatlan/pkg/templates/default_template.jinja2 index 9a030a61e..f86a3e417 100644 --- a/pyatlan/pkg/templates/default_template.jinja2 +++ b/pyatlan/pkg/templates/default_template.jinja2 @@ -31,9 +31,11 @@ spec: {%- if not ns.has_s3 %} artifacts: [] {% endif %} +{%- for name, value in pkg.outputs.items() %} + {%- if loop.first %} outputs: artifacts: -{%- for name, value in pkg.outputs.items() %} + {%- endif %} - name: {{ name }} path: {{ value }} {%- endfor %} From 4353d0c490d64e4cdee75ad5a99de3e932db7e65 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 4 Jan 2024 11:16:19 +0300 Subject: [PATCH 34/56] Update version string --- pyatlan/pkg/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 558a9b41a..4d6151773 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -95,7 +95,7 @@ def __init__(self, **data): ) self._package_definition = _PackageDefinition( name=self.package_id, - version="1.6.5", + version="1.9.0-SNAPSHOT", description=self.description, keywords=self.keywords, homepage=f"https://packages.atlan.com/-/web/detail/{self.package_id}", From f6c158f84768588af2b423869d84d19b0b694aa1 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Mon, 8 Jan 2024 13:37:28 +0300 Subject: [PATCH 35/56] Fix problem --- pyatlan/pkg/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 4d6151773..caab18b08 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -91,7 +91,7 @@ def __init__(self, **data): super().__init__(**data) source = self.connector_type.value if self.connector_type else "atlan" source_category = ( - self.connector_type.value if self.connector_type else "utility" + self.connector_type.category.value if self.connector_type else "utility" ) self._package_definition = _PackageDefinition( name=self.package_id, From 82e7a88ef30e18b68edd6c5737075266624792a3 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Mon, 8 Jan 2024 16:12:47 +0300 Subject: [PATCH 36/56] Fix problems --- pyatlan/pkg/templates/default_configmap.jinja2 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyatlan/pkg/templates/default_configmap.jinja2 b/pyatlan/pkg/templates/default_configmap.jinja2 index c99877e06..0cf0dbc75 100644 --- a/pyatlan/pkg/templates/default_configmap.jinja2 +++ b/pyatlan/pkg/templates/default_configmap.jinja2 @@ -8,10 +8,14 @@ data: "properties": { {%- for key, value in pkg.ui_config.properties.items() %} "{{ key }}": { - "type": {{ value.type_ }} - "required": {{ value.required | tojson}} + "type": "{{ value.type_ }}", + "required": {{ value.required | tojson}}, + {%- if value.possible_values %} + "enum": [ {%- for val in value.possible_values.keys() %}{{ " " }}"{{ val }}"{%- if not loop.last %},{%- endif %}{% endfor -%}{{ " ]" }}, + "enumNames": [ {%- for val in value.possible_values.values() %}{{ " " }}"{{ val }}"{%- if not loop.last %},{%- endif %}{% endfor -%}{{ " ]" }}, + {%- endif %} "ui": {{ value.ui.to_json() | indent(10)}} - } + }{%- if not loop.last %},{%- endif %} {%- endfor %} }, "anyOf": [ @@ -21,7 +25,7 @@ data: ], "steps": [ {%- for step in pkg.ui_config.steps %} - {{ step.to_json() | indent(10) }} + {{ step.to_json() | indent(10) }}{%- if not loop.last %},{%- endif %} {%- endfor %} ] } From 7fb8583773ed1bf7fcd6bce16dedec2201616682 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 9 Jan 2024 09:44:29 +0300 Subject: [PATCH 37/56] Add code to create class --- pyatlan/pkg/models.py | 8 ++- pyatlan/pkg/templates/package_config.jinja2 | 75 +++++++++++++++++++++ pyatlan/pkg/widgets.py | 36 ++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 pyatlan/pkg/templates/package_config.jinja2 diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index caab18b08..e2c182558 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -264,6 +264,12 @@ def create_configmaps(self): with (self._config_maps_dir / "default.yaml").open("w") as script: script.write(content) + def create_config_class(self): + template = self._env.get_template("package_config.jinja2") + content = template.render({"pkg": self.pkg}) + with (self.path / f"{self.pkg.package_name}Cfg.py").open("w") as script: + script.write(content) + @validate_arguments() def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "config"]): @@ -271,4 +277,4 @@ def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "conf if operation == "package": writer.create_package() else: - writer.create_configmaps() + writer.create_config_class() diff --git a/pyatlan/pkg/templates/package_config.jinja2 b/pyatlan/pkg/templates/package_config.jinja2 new file mode 100644 index 000000000..138815d1c --- /dev/null +++ b/pyatlan/pkg/templates/package_config.jinja2 @@ -0,0 +1,75 @@ +from datetime import datetime +from pydantic import BaseModel, BaseSettings, Field, parse_obj_as, validator +from pyatlan.model.assets import Connection +from pyatlan.model.enums import AtlanConnectorType +from typing import Any, Optional, Union +import json +import os + +ENV = 'env' + + +def validate_multiselect(v, values, **kwargs): + if isinstance(v, str): + if v.startswith('['): + data = json.loads(v) + v = parse_obj_as(list[str], data) + else: + v = [v] + return v + + +def validate_connection(v, values, config, field, **kwargs): + v = Connection.parse_raw(v) + + +class ConnectorAndConnection(BaseModel): + source: AtlanConnectorType + connections: list[str] + + +def validate_connector_and_connection(v, values, config, field, **kwargs): + return ConnectorAndConnection.parse_raw(v) + +class CustomConfig(BaseModel): + """""""" +{%- for key, value in pkg.ui_config.properties.items() %} + {{ value.ui.get_validator(key) }} +{%- endfor %} + +class RuntimeConfig(BaseSettings): + user_id:Optional[str] = Field(default="") + agent:Optional[str] = Field(default="") + agent_id:Optional[str] = Field(default="") + agent_pkg:Optional[str] = Field(default="") + agent_wfl:Optional[str] = Field(default="") + custom_config:Optional[CustomConfig] = None + + class Config: + fields = { + 'user_id': { + ENV: 'ATLAN_USER_ID', + }, + 'agent': { + ENV: 'X_ATLAN_AGENT' + }, + 'agent_id': { + ENV: 'X_ATLAN_AGENT_ID' + }, + 'agent_pkg': { + ENV: 'X_ATLAN_AGENT_PACKAGE_NAME' + }, + 'custom_config': { + ENV: 'NESTED_CONFIG' + } + } + @classmethod + def parse_env_var(cls, field_name:str, raw_value:str)->Any: + if field_name == 'custom_config': + return CustomConfig.parse_raw(raw_value) + return cls.json_loads(raw_value) + +if __name__ == "__main__": + print(os.environ["NESTED_CONFIG"]) + r = RuntimeConfig() + print(r.json()) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index b61601f35..769b613ce 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -66,6 +66,9 @@ def parameter_value(self) -> str: def s3_artifact(self) -> bool: return False + def get_validator(self, name: str): + return f"{name}: str\n" + @dataclass class AbstractUIElement(abc.ABC): @@ -167,6 +170,9 @@ def to_nested(self, name: str) -> str: def parameter_value(self) -> str: return "false" + def get_validator(self, name: str): + return f"{name}: bool = None\n" + @dataclass class BooleanInput(AbstractUIElement): @@ -205,6 +211,14 @@ def __init__( placeholder=placeholder, ) + def get_validator(self, name: str): + return ( + f'{name}: Optional[Connection] = None\n")' + f"_validate_{name} = validator(" + "connection" + ', pre=True, allow_reuse=True)(validate_connection)\n")' + ) + @dataclass class ConnectionCreator(AbstractUIElement): @@ -317,6 +331,14 @@ def __init__( ) self.start = start + def get_validator(self, name: str): + return ( + f'{name}: ConnectorAndConnection = Field(alias="connectorType")\n' + f"_validate_{name} = validator(" + "connection" + ', pre=True, allow_reuse=True)(validate_connector_and_connection)\n")' + ) + @dataclass class ConnectorTypeSelector(AbstractUIElement): @@ -386,6 +408,9 @@ def to_nested(self, name: str) -> str: def parameter_value(self) -> str: return "-1" + def get_validator(self, name: str): + return f"{name}: Optional[datetime] = None\n" + @dataclass class DateInput(AbstractUIElement): @@ -620,6 +645,14 @@ def __init__(self, label: str, hidden: bool = False, help: str = "", grid: int = grid=grid, ) + def get_validator(self, name: str): + return ( + f'{name}:Optional[list[str]] = Field(default_factory=list)\n")' + f"_validate_{name} = validator(" + "{name}" + ', pre=True, allow_reuse=True)(validate_multiselect)\n")' + ) + @dataclass class MultipleGroups(AbstractUIElement): @@ -717,6 +750,9 @@ def to_nested(self, name: str) -> str: def parameter_value(self) -> str: return "-1" + def get_validator(self, name: str): + return f"{name}: Optional[Union[int,float]] = None\n" + @dataclass class NumericInput(AbstractUIElement): From d303fbe9626da24e9cbb042fd12c9c21b8091723 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 10:23:47 +0300 Subject: [PATCH 38/56] Add impersonation client --- pyatlan/client/atlan.py | 8 +++ pyatlan/client/constants.py | 7 +++ pyatlan/client/impersonate.py | 38 ++++++++++++ pyatlan/errors.py | 7 +++ pyatlan/model/response.py | 11 ++++ pyatlan/pkg/models.py | 4 +- pyatlan/pkg/templates/__init__.py | 2 + pyatlan/pkg/templates/package_config.jinja2 | 68 +++++++++++---------- pyatlan/pkg/utils.py | 13 ++++ pyatlan/pkg/widgets.py | 8 +-- pyatlan/version.txt | 2 +- setup.py | 2 +- tests/unit/pkg/test_models.py | 2 +- tests/unit/test_typedef_model.py | 1 - 14 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 pyatlan/client/impersonate.py create mode 100644 pyatlan/pkg/templates/__init__.py diff --git a/pyatlan/client/atlan.py b/pyatlan/client/atlan.py index b232cfe03..abc85fb61 100644 --- a/pyatlan/client/atlan.py +++ b/pyatlan/client/atlan.py @@ -34,6 +34,7 @@ from pyatlan.client.constants import PARSE_QUERY, UPLOAD_IMAGE from pyatlan.client.credential import CredentialClient from pyatlan.client.group import GroupClient +from pyatlan.client.impersonate import ImpersonationClient from pyatlan.client.role import RoleClient from pyatlan.client.search_log import SearchLogClient from pyatlan.client.token import TokenClient @@ -133,6 +134,7 @@ class AtlanClient(BaseSettings): _typedef_client: Optional[TypeDefClient] = PrivateAttr(default=None) _token_client: Optional[TokenClient] = PrivateAttr(default=None) _user_client: Optional[UserClient] = PrivateAttr(default=None) + _asset_client: Optional[ImpersonationClient] = PrivateAttr(default=None) class Config: env_prefix = "atlan_" @@ -218,6 +220,12 @@ def asset(self) -> AssetClient: self._asset_client = AssetClient(client=self) return self._asset_client + @property + def impersonate(self) -> ImpersonationClient: + if self._impersonate_client is None: + self._impersonate_client = ImpersonationClient(client=self) + return self._asset_client + @property def token(self) -> TokenClient: if self._token_client is None: diff --git a/pyatlan/client/constants.py b/pyatlan/client/constants.py index d5df65f01..8f3be279b 100644 --- a/pyatlan/client/constants.py +++ b/pyatlan/client/constants.py @@ -110,6 +110,13 @@ TOKENS_API, HTTPMethod.DELETE, HTTPStatus.OK, endpoint=EndPoint.HERACLES ) +GET_TOKEN = API( + "/auth/realms/default/protocol/openid-connect/token", + HTTPMethod.POST, + HTTPStatus.OK, + endpoint=EndPoint.IMPERSONATION, +) + ENTITY_API = "entity/" PREFIX_ATTR = "attr:" PREFIX_ATTR_ = "attr_" diff --git a/pyatlan/client/impersonate.py b/pyatlan/client/impersonate.py new file mode 100644 index 000000000..6c92a718c --- /dev/null +++ b/pyatlan/client/impersonate.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2022 Atlan Pte. Ltd. + +import os + +from pyatlan.client.common import ApiCaller +from pyatlan.client.constants import GET_TOKEN +from pyatlan.errors import ErrorCode +from pyatlan.model.response import AccessTokenResponse + + +class ImpersonationClient: + """ + This class can be used for impersonating users as part of Atlan automations (if desired). + Note: this will only work when run as part of Atlan's packaged workflow ecosystem (running in the cluster back-end). + """ + + def __init__(self, client: ApiCaller): + if not isinstance(client, ApiCaller): + raise ErrorCode.INVALID_PARAMETER_TYPE.exception_with_parameters( + "client", "ApiCaller" + ) + self._client = client + + def user(self, user_id: str) -> str: + client_id = os.getenv("CLIENT_ID") + client_secret = os.getenv("CLIENT_SECRET") + if not client_id or client_secret: + raise ErrorCode.MISSING_CREDENTIALS.exception_with_parameters() + credentials = { + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + } + + raw_json = self._client._call_api(GET_TOKEN, request_obj=credentials) + + return AccessTokenResponse(**raw_json) diff --git a/pyatlan/errors.py b/pyatlan/errors.py index 9223a5205..3f6d19232 100644 --- a/pyatlan/errors.py +++ b/pyatlan/errors.py @@ -477,6 +477,13 @@ class ErrorCode(Enum): "Please double-check your credentials and test them again.", InvalidRequestError, ) + MISSING_CREDENTIALS = ( + 400, + "ATLAN-PYTHON-400-044", + "Missing privileged credentials to impersonate users.", + "You must have both CLIENT_ID and CLIENT_SECRET configured to be able to impersonate users.", + InvalidRequestError, + ) AUTHENTICATION_PASSTHROUGH = ( 401, "ATLAN-PYTHON-401-000", diff --git a/pyatlan/model/response.py b/pyatlan/model/response.py index 868e9ab50..26217e631 100644 --- a/pyatlan/model/response.py +++ b/pyatlan/model/response.py @@ -85,3 +85,14 @@ def assets_partially_updated(self, asset_type: Type[A]) -> list[A]: if isinstance(asset, asset_type) ] return [] + + +class AccessTokenResponse(AtlanObject): + access_token: str + expires_in: int + refresh_expires_in: int + refresh_token: str + token_type: str + not_before_policy: int + session_state: str + scope: str diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index e2c182558..e16ac5690 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -267,7 +267,9 @@ def create_configmaps(self): def create_config_class(self): template = self._env.get_template("package_config.jinja2") content = template.render({"pkg": self.pkg}) - with (self.path / f"{self.pkg.package_name}Cfg.py").open("w") as script: + with ( + self.path / f"{self.pkg.package_name.replace(' ','_').lower()}_cfg.py" + ).open("w") as script: script.write(content) diff --git a/pyatlan/pkg/templates/__init__.py b/pyatlan/pkg/templates/__init__.py new file mode 100644 index 000000000..6be70a41e --- /dev/null +++ b/pyatlan/pkg/templates/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2022 Atlan Pte. Ltd. diff --git a/pyatlan/pkg/templates/package_config.jinja2 b/pyatlan/pkg/templates/package_config.jinja2 index 138815d1c..d3eed00a5 100644 --- a/pyatlan/pkg/templates/package_config.jinja2 +++ b/pyatlan/pkg/templates/package_config.jinja2 @@ -4,8 +4,14 @@ from pyatlan.model.assets import Connection from pyatlan.model.enums import AtlanConnectorType from typing import Any, Optional, Union import json +import logging.config import os +if resources.is_resource("pyatlan", "logging.conf"): + with resources.open_text("ernest", "logging.conf") as logging_conf: + logging.config.fileConfig(logging_conf) +LOGGER = logging.getLogger(__name__) + ENV = 'env' @@ -38,38 +44,38 @@ class CustomConfig(BaseModel): {%- endfor %} class RuntimeConfig(BaseSettings): - user_id:Optional[str] = Field(default="") - agent:Optional[str] = Field(default="") - agent_id:Optional[str] = Field(default="") - agent_pkg:Optional[str] = Field(default="") - agent_wfl:Optional[str] = Field(default="") - custom_config:Optional[CustomConfig] = None + user_id:Optional[str] = Field(default="") + agent:Optional[str] = Field(default="") + agent_id:Optional[str] = Field(default="") + agent_pkg:Optional[str] = Field(default="") + agent_wfl:Optional[str] = Field(default="") + custom_config:Optional[CustomConfig] = None - class Config: - fields = { - 'user_id': { - ENV: 'ATLAN_USER_ID', - }, - 'agent': { - ENV: 'X_ATLAN_AGENT' - }, - 'agent_id': { - ENV: 'X_ATLAN_AGENT_ID' - }, - 'agent_pkg': { - ENV: 'X_ATLAN_AGENT_PACKAGE_NAME' - }, - 'custom_config': { - ENV: 'NESTED_CONFIG' - } - } - @classmethod - def parse_env_var(cls, field_name:str, raw_value:str)->Any: - if field_name == 'custom_config': - return CustomConfig.parse_raw(raw_value) - return cls.json_loads(raw_value) + class Config: + fields = { + 'user_id': { + ENV: 'ATLAN_USER_ID', + }, + 'agent': { + ENV: 'X_ATLAN_AGENT' + }, + 'agent_id': { + ENV: 'X_ATLAN_AGENT_ID' + }, + 'agent_pkg': { + ENV: 'X_ATLAN_AGENT_PACKAGE_NAME' + }, + 'custom_config': { + ENV: 'NESTED_CONFIG' + } + } + @classmethod + def parse_env_var(cls, field_name:str, raw_value:str)->Any: + if field_name == 'custom_config': + return CustomConfig.parse_raw(raw_value) + return cls.json_loads(raw_value) if __name__ == "__main__": - print(os.environ["NESTED_CONFIG"]) + LOGGER.info(os.environ["NESTED_CONFIG"]) r = RuntimeConfig() - print(r.json()) + LOGGER.info(r.json()) diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index b79be790b..cb5c3844f 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2023 Atlan Pte. Ltd. +import logging +import os from pathlib import Path from jinja2 import Environment, PackageLoader @@ -7,6 +9,8 @@ from pyatlan.pkg.models import CustomPackage +LOGGER = logging.getLogger(__name__) + class PackageWriter(BaseModel): path: str @@ -40,3 +44,12 @@ def create_templates(self): content = template.render({"pkg": self.pkg}) with (self._templates_dir / "default.yaml").open("w") as script: script.write(content) + + +def set_client(impersonate_user_id: str): + os.get("ATLAN_BASE_URL", "INTERNAL") + api_token = os.get("ATLAN_API_KEY", "") + os.get("ATLAN_USER_ID", impersonate_user_id) + if token_to_use := api_token: + LOGGER.info("Using provided API token for authentication.") + print(token_to_use) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 769b613ce..cf723ed99 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -213,10 +213,10 @@ def __init__( def get_validator(self, name: str): return ( - f'{name}: Optional[Connection] = None\n")' - f"_validate_{name} = validator(" - "connection" - ', pre=True, allow_reuse=True)(validate_connection)\n")' + f"{name}: Optional[Connection] = None\n" + f" _validate_{name} = validator(" + '"connection"' + ", pre=True, allow_reuse=True)(validate_connection)\n" ) diff --git a/pyatlan/version.txt b/pyatlan/version.txt index bfa363e76..d327595aa 100644 --- a/pyatlan/version.txt +++ b/pyatlan/version.txt @@ -1 +1 @@ -1.8.4 +1.8.5-a.1 diff --git a/setup.py b/setup.py index ecece528d..fd2f44127 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ def read(file_name): "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", ], - package_data={"pyatlan": ["py.typed"]}, + package_data={"pyatlan": ["py.typed", "logging.conf"], "": ["*.jinja2"]}, packages=find_packages(), install_requires=requirements, extra_requires={"dev": extra}, diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py index 01422f434..01e36d162 100644 --- a/tests/unit/pkg/test_models.py +++ b/tests/unit/pkg/test_models.py @@ -197,4 +197,4 @@ def test_generate_with_operation_package(mock_package_writer, custom_package): def test_generate_with_operation_config(mock_package_writer, custom_package): generate(pkg=custom_package, path="..", operation="config") - mock_package_writer.create_configmaps.assert_called() + mock_package_writer.create_config_class.assert_called() diff --git a/tests/unit/test_typedef_model.py b/tests/unit/test_typedef_model.py index 34c82cd10..8341c0ce1 100644 --- a/tests/unit/test_typedef_model.py +++ b/tests/unit/test_typedef_model.py @@ -274,7 +274,6 @@ def test_type_def_response(type_defs): class TestAttributeDef: @pytest.fixture() def sut(self) -> AttributeDef: - with patch("pyatlan.model.typedef._get_all_qualified_names") as mock_get_qa: mock_get_qa.return_value = set() return AttributeDef.create( From 4e1df0d2bfecb2be870225793552f42438406adb Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 10:31:13 +0300 Subject: [PATCH 39/56] Corect typo --- pyatlan/client/atlan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatlan/client/atlan.py b/pyatlan/client/atlan.py index abc85fb61..46967aec7 100644 --- a/pyatlan/client/atlan.py +++ b/pyatlan/client/atlan.py @@ -134,7 +134,7 @@ class AtlanClient(BaseSettings): _typedef_client: Optional[TypeDefClient] = PrivateAttr(default=None) _token_client: Optional[TokenClient] = PrivateAttr(default=None) _user_client: Optional[UserClient] = PrivateAttr(default=None) - _asset_client: Optional[ImpersonationClient] = PrivateAttr(default=None) + _impersonate_client: Optional[ImpersonationClient] = PrivateAttr(default=None) class Config: env_prefix = "atlan_" From e7490d9a0dc08a4366ffcc439ead068a447c507b Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 10:32:32 +0300 Subject: [PATCH 40/56] Corect typo --- pyatlan/client/atlan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatlan/client/atlan.py b/pyatlan/client/atlan.py index 46967aec7..badedbd1c 100644 --- a/pyatlan/client/atlan.py +++ b/pyatlan/client/atlan.py @@ -224,7 +224,7 @@ def asset(self) -> AssetClient: def impersonate(self) -> ImpersonationClient: if self._impersonate_client is None: self._impersonate_client = ImpersonationClient(client=self) - return self._asset_client + return self._impersonate_client @property def token(self) -> TokenClient: From 73537266a7a50baf24b9ac9aa0b4300b0c0ec5a2 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 10:53:34 +0300 Subject: [PATCH 41/56] Corect typo --- pyatlan/client/impersonate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatlan/client/impersonate.py b/pyatlan/client/impersonate.py index 6c92a718c..355a345ab 100644 --- a/pyatlan/client/impersonate.py +++ b/pyatlan/client/impersonate.py @@ -25,7 +25,7 @@ def __init__(self, client: ApiCaller): def user(self, user_id: str) -> str: client_id = os.getenv("CLIENT_ID") client_secret = os.getenv("CLIENT_SECRET") - if not client_id or client_secret: + if not client_id or not client_secret: raise ErrorCode.MISSING_CREDENTIALS.exception_with_parameters() credentials = { "grant_type": "client_credentials", From c9910968040bdf8bfa107f5c917ede373f918ede Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 14:55:45 +0300 Subject: [PATCH 42/56] Fix handler of imporsonation --- pyatlan/__init__.py | 9 ++++----- pyatlan/client/atlan.py | 10 +++++++++- pyatlan/client/constants.py | 3 +++ pyatlan/logging.conf | 4 ++-- pyatlan/model/response.py | 2 +- pyatlan/utils.py | 2 ++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pyatlan/__init__.py b/pyatlan/__init__.py index ec13c3d5b..49eeb1497 100644 --- a/pyatlan/__init__.py +++ b/pyatlan/__init__.py @@ -1,14 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2022 Atlan Pte. Ltd. import logging.config -import os -from logging import NullHandler +from importlib import resources from pyatlan.utils import REQUEST_ID_FILTER +if resources.is_resource("pyatlan", "logging.conf"): + with resources.open_text("pyatlan", "logging.conf") as logging_conf: + logging.config.fileConfig(logging_conf) LOGGER = logging.getLogger(__name__) -LOGGER.addHandler(NullHandler()) -if os.path.exists("logging.conf"): - logging.config.fileConfig("logging.conf") for handler in LOGGER.handlers: handler.addFilter(REQUEST_ID_FILTER) diff --git a/pyatlan/client/atlan.py b/pyatlan/client/atlan.py index badedbd1c..e17fc6e0a 100644 --- a/pyatlan/client/atlan.py +++ b/pyatlan/client/atlan.py @@ -65,7 +65,13 @@ from pyatlan.model.typedef import TypeDef, TypeDefResponse from pyatlan.model.user import AtlanUser, UserMinimalResponse, UserResponse from pyatlan.multipart_data_generator import MultipartDataGenerator -from pyatlan.utils import API, AuthorizationFilter, HTTPStatus, RequestIdAdapter +from pyatlan.utils import ( + API, + APPLICATION_ENCODED_FORM, + AuthorizationFilter, + HTTPStatus, + RequestIdAdapter, +) request_id_var = ContextVar("request_id", default=None) @@ -359,6 +365,8 @@ def _create_params( params["data"] = request_obj.json( by_alias=True, exclude_unset=exclude_unset ) + elif api.consumes == APPLICATION_ENCODED_FORM: + params["data"] = request_obj else: params["data"] = json.dumps(request_obj) return params diff --git a/pyatlan/client/constants.py b/pyatlan/client/constants.py index 8f3be279b..762ca5ee2 100644 --- a/pyatlan/client/constants.py +++ b/pyatlan/client/constants.py @@ -3,6 +3,7 @@ # Based on original code from https://github.com/apache/atlas (under Apache-2.0 license) from pyatlan.utils import ( API, + APPLICATION_ENCODED_FORM, APPLICATION_JSON, APPLICATION_OCTET_STREAM, MULTIPART_FORM_DATA, @@ -115,6 +116,8 @@ HTTPMethod.POST, HTTPStatus.OK, endpoint=EndPoint.IMPERSONATION, + consumes=APPLICATION_ENCODED_FORM, + produces=APPLICATION_ENCODED_FORM, ) ENTITY_API = "entity/" diff --git a/pyatlan/logging.conf b/pyatlan/logging.conf index 7716d5701..710c803d2 100644 --- a/pyatlan/logging.conf +++ b/pyatlan/logging.conf @@ -13,7 +13,7 @@ handlers=consoleHandler [logger_pyatlan] level=DEBUG -handlers=consoleHandler,fileHandler,jsonHandler +handlers=fileHandler,jsonHandler qualname=pyatlan propagate=0 @@ -32,7 +32,7 @@ args=(sys.stdout,) class=FileHandler level=DEBUG formatter=simpleFormatter -args=('/tmp/pyatlan.log',) +args=('/tmp/debug.log',) [handler_jsonHandler] class=FileHandler diff --git a/pyatlan/model/response.py b/pyatlan/model/response.py index 26217e631..dcd7c9240 100644 --- a/pyatlan/model/response.py +++ b/pyatlan/model/response.py @@ -93,6 +93,6 @@ class AccessTokenResponse(AtlanObject): refresh_expires_in: int refresh_token: str token_type: str - not_before_policy: int + not_before_policy: Optional[int] = Field(default=None) session_state: str scope: str diff --git a/pyatlan/utils.py b/pyatlan/utils.py index 5d80d5d21..80ef7f026 100644 --- a/pyatlan/utils.py +++ b/pyatlan/utils.py @@ -23,6 +23,8 @@ REQUESTID = "requestid" APPLICATION_JSON = "application/json" +APPLICATION_ENCODED_FORM = "application/x-www-form-urlencoded;charset=UTF-8" + APPLICATION_OCTET_STREAM = "application/octet-stream" MULTIPART_FORM_DATA = "multipart/form-data" PREFIX_ATTR = "attr:" From d9dda61631092fb2bfbdc184bbcb94da903011b4 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 17:57:06 +0300 Subject: [PATCH 43/56] Fix user impersonation --- pyatlan/client/impersonate.py | 34 ++++++++++++++++++++++++++++++---- pyatlan/errors.py | 14 ++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/pyatlan/client/impersonate.py b/pyatlan/client/impersonate.py index 355a345ab..4708bffdd 100644 --- a/pyatlan/client/impersonate.py +++ b/pyatlan/client/impersonate.py @@ -1,13 +1,16 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2022 Atlan Pte. Ltd. +import logging import os from pyatlan.client.common import ApiCaller from pyatlan.client.constants import GET_TOKEN -from pyatlan.errors import ErrorCode +from pyatlan.errors import AtlanError, ErrorCode from pyatlan.model.response import AccessTokenResponse +LOGGER = logging.getLogger(__name__) + class ImpersonationClient: """ @@ -23,6 +26,13 @@ def __init__(self, client: ApiCaller): self._client = client def user(self, user_id: str) -> str: + """ + Retrieves a bearer token that impersonates the provided user. + + :param user_id: unique identifier of the user to impersonate + :returns: a bearer token that impersonates the provided user + :raises AtlanError: on any API communication issue + """ client_id = os.getenv("CLIENT_ID") client_secret = os.getenv("CLIENT_SECRET") if not client_id or not client_secret: @@ -33,6 +43,22 @@ def user(self, user_id: str) -> str: "client_secret": client_secret, } - raw_json = self._client._call_api(GET_TOKEN, request_obj=credentials) - - return AccessTokenResponse(**raw_json) + LOGGER.debug("Getting token with client id and secret") + try: + raw_json = self._client._call_api(GET_TOKEN, request_obj=credentials) + argo_token = AccessTokenResponse(**raw_json).access_token + except AtlanError as atlan_err: + raise ErrorCode.UNABLE_TO_ESCALATE.exception_with_parameters() from atlan_err + LOGGER.debug("Getting token with subject token") + try: + user_credentials = { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "client_id": client_id, + "client_secret": client_secret, + "subject_token": argo_token, + "requested_subject": user_id, + } + raw_json = self._client._call_api(GET_TOKEN, request_obj=user_credentials) + return AccessTokenResponse(**raw_json).access_token + except AtlanError as atlan_err: + raise ErrorCode.UNABLE_TO_IMPERSONATE.exception_with_parameters() from atlan_err diff --git a/pyatlan/errors.py b/pyatlan/errors.py index 3f6d19232..eb516b948 100644 --- a/pyatlan/errors.py +++ b/pyatlan/errors.py @@ -535,6 +535,20 @@ class ErrorCode(Enum): "Check the details of the server's message to correct your request.", PermissionError, ) + UNABLE_TO_ESCALATE = ( + 403, + "ATLAN-PYTHON-403-001", + "Unable to escalate to a privileged user.", + "Check the details of your configured privileged credentials.", + PermissionError, + ) + UNABLE_TO_IMPERSONATE = ( + 403, + "ATLAN-PYTHON-403-002", + "Unable to impersonate requested user.", + "Check the details of your configured privileged credentials and the user you requested to impersonate.", + PermissionError, + ) NOT_FOUND_PASSTHROUGH = ( 404, "ATLAN-PYTHON-404-000", From 2e310348a6844ca54f1a09a71d41feabb0308904 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 19:03:35 +0300 Subject: [PATCH 44/56] Add additional logging --- pyatlan/client/impersonate.py | 48 +++++++++++++++++++++++++++++------ pyatlan/pkg/utils.py | 25 +++++++++++++----- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/pyatlan/client/impersonate.py b/pyatlan/client/impersonate.py index 4708bffdd..852cefb68 100644 --- a/pyatlan/client/impersonate.py +++ b/pyatlan/client/impersonate.py @@ -3,6 +3,7 @@ import logging import os +from typing import NamedTuple from pyatlan.client.common import ApiCaller from pyatlan.client.constants import GET_TOKEN @@ -12,6 +13,11 @@ LOGGER = logging.getLogger(__name__) +class ClientInfo(NamedTuple): + client_id: str + client_secret: str + + class ImpersonationClient: """ This class can be used for impersonating users as part of Atlan automations (if desired). @@ -33,14 +39,11 @@ def user(self, user_id: str) -> str: :returns: a bearer token that impersonates the provided user :raises AtlanError: on any API communication issue """ - client_id = os.getenv("CLIENT_ID") - client_secret = os.getenv("CLIENT_SECRET") - if not client_id or not client_secret: - raise ErrorCode.MISSING_CREDENTIALS.exception_with_parameters() + client_info = self._get_client_info() credentials = { "grant_type": "client_credentials", - "client_id": client_id, - "client_secret": client_secret, + "client_id": client_info.client_id, + "client_secret": client_info.client_secret, } LOGGER.debug("Getting token with client id and secret") @@ -53,8 +56,8 @@ def user(self, user_id: str) -> str: try: user_credentials = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", - "client_id": client_id, - "client_secret": client_secret, + "client_id": client_info.client_id, + "client_secret": client_info.client_secret, "subject_token": argo_token, "requested_subject": user_id, } @@ -62,3 +65,32 @@ def user(self, user_id: str) -> str: return AccessTokenResponse(**raw_json).access_token except AtlanError as atlan_err: raise ErrorCode.UNABLE_TO_IMPERSONATE.exception_with_parameters() from atlan_err + + def _get_client_info(self) -> ClientInfo: + client_id = os.getenv("CLIENT_ID") + client_secret = os.getenv("CLIENT_SECRET") + if not client_id or not client_secret: + raise ErrorCode.MISSING_CREDENTIALS.exception_with_parameters() + client_info = ClientInfo(client_id=client_id, client_secret=client_secret) + return client_info + + def escolate(self) -> str: + """ + Escalate to a privileged user on a short-term basis. + Note: this is only possible from within the Atlan tenant, and only when given the appropriate credentials. + + :returns: a short-lived bearer token with escalated privileges + :raises AtlanError: on any API communication issue + """ + client_info = self._get_client_info() + credentials = { + "grant_type": "client_credentials", + "client_id": client_info.client_id, + "client_secret": client_info.client_secret, + "scope": "openid", + } + try: + raw_json = self._client._call_api(GET_TOKEN, request_obj=credentials) + return AccessTokenResponse(**raw_json).access_token + except AtlanError as atlan_err: + raise ErrorCode.UNABLE_TO_ESCALATE.exception_with_parameters() from atlan_err diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index cb5c3844f..16a4c3506 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -7,6 +7,7 @@ from jinja2 import Environment, PackageLoader from pydantic import BaseModel, PrivateAttr +from pyatlan.client.atlan import AtlanClient from pyatlan.pkg.models import CustomPackage LOGGER = logging.getLogger(__name__) @@ -46,10 +47,22 @@ def create_templates(self): script.write(content) -def set_client(impersonate_user_id: str): - os.get("ATLAN_BASE_URL", "INTERNAL") - api_token = os.get("ATLAN_API_KEY", "") - os.get("ATLAN_USER_ID", impersonate_user_id) - if token_to_use := api_token: +def set_client(impersonate_user_id: str) -> AtlanClient: + + base_url = os.environ.get("ATLAN_BASE_URL", "INTERNAL") + api_token = os.environ.get("ATLAN_API_KEY", "") + user_id = os.environ.get("ATLAN_USER_ID", impersonate_user_id) + if api_token: LOGGER.info("Using provided API token for authentication.") - print(token_to_use) + api_key = api_token + elif user_id: + LOGGER.info("No API token found, attempting to impersonate user: %s", user_id) + api_key = AtlanClient(base_url=base_url, api_key="").impersonate.user( + user_id=user_id + ) + else: + LOGGER.info( + "No API token or impersonation user, attempting short-lived escalation." + ) + api_key = AtlanClient(base_url=base_url, api_key="").impersonate.escolate() + return AtlanClient(base_url=base_url, api_key=api_key) From 0ce51830784cec1144d1b6979ca895a7ac0466fc Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Thu, 11 Jan 2024 19:06:04 +0300 Subject: [PATCH 45/56] Update documentation --- pyatlan/pkg/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index 16a4c3506..ff69eab01 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -48,7 +48,13 @@ def create_templates(self): def set_client(impersonate_user_id: str) -> AtlanClient: + """ + Set up the default Atlan client, based on environment variables. + This will use an API token if found in ATLAN_API_KEY, and will fallback to attempting to impersonate a user if + ATLAN_API_KEY is empty. + :param impersonate_user_id: unique identifier (GUID) of a user or API token to impersonate + """ base_url = os.environ.get("ATLAN_BASE_URL", "INTERNAL") api_token = os.environ.get("ATLAN_API_KEY", "") user_id = os.environ.get("ATLAN_USER_ID", impersonate_user_id) From 85e1913ff3284ab1c9504e28e827a93c1e88d7dd Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Mon, 15 Jan 2024 13:30:45 +0300 Subject: [PATCH 46/56] Add additional methods --- pyatlan/client/atlan.py | 3 +++ pyatlan/client/token.py | 14 ++++++++++++++ pyatlan/pkg/models.py | 10 +++++++++- pyatlan/pkg/templates/package_config.jinja2 | 3 +++ pyatlan/pkg/utils.py | 21 +++++++++++++++++++-- 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/pyatlan/client/atlan.py b/pyatlan/client/atlan.py index e17fc6e0a..a49f71b7f 100644 --- a/pyatlan/client/atlan.py +++ b/pyatlan/client/atlan.py @@ -250,6 +250,9 @@ def user(self) -> UserClient: self._user_client = UserClient(client=self) return self._user_client + def update_headers(self, header: dict[str, str]): + self._session.headers.update(header) + def _call_api_internal(self, api, path, params, binary_data=None): token = request_id_var.set(str(uuid.uuid4())) try: diff --git a/pyatlan/client/token.py b/pyatlan/client/token.py index bb7b91c98..5b3d3de76 100644 --- a/pyatlan/client/token.py +++ b/pyatlan/client/token.py @@ -93,6 +93,20 @@ def get_by_id(self, client_id: str) -> Optional[ApiToken]: return response.records[0] return None + def get_by_guid(self, guid: str) -> Optional[ApiToken]: + """ + Retrieves the API token with a unique ID (GUID) that exactly matches the provided string. + + :param guid: unique identifier by which to retrieve the API token + :returns: the API token whose clientId matches the provided string, or None if there is none + """ + if response := self.get( + offset=0, limit=5, post_filter='{"id":"' + guid + '"}', sort="createdAt" + ): + if response.records and len(response.records) >= 1: + return response.records[0] + return None + def create( self, display_name: str, diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index e16ac5690..2b27fcd7b 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -5,7 +5,7 @@ import textwrap from enum import Enum from pathlib import Path -from typing import Literal, Optional +from typing import Literal, Optional, Protocol from jinja2 import Environment, PackageLoader from pydantic import BaseModel, Field, PrivateAttr, StrictStr, validate_arguments @@ -16,6 +16,14 @@ LOGGER = logging.getLogger(__name__) +class RuntimeConfig(Protocol): + user_id: Optional[str] + agent: Optional[str] + agent_id: Optional[str] + agent_pkg: Optional[str] + agent_wfl: Optional[str] + + class PackageConfig(BaseModel): labels: dict[StrictStr, StrictStr] annotations: dict[StrictStr, StrictStr] diff --git a/pyatlan/pkg/templates/package_config.jinja2 b/pyatlan/pkg/templates/package_config.jinja2 index d3eed00a5..4960328b5 100644 --- a/pyatlan/pkg/templates/package_config.jinja2 +++ b/pyatlan/pkg/templates/package_config.jinja2 @@ -65,6 +65,9 @@ class RuntimeConfig(BaseSettings): 'agent_pkg': { ENV: 'X_ATLAN_AGENT_PACKAGE_NAME' }, + 'agent_wfl': { + ENV: 'X_ATLAN_AGENT_WORKFLOW_ID' + } 'custom_config': { ENV: 'NESTED_CONFIG' } diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index ff69eab01..77924cd59 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, PrivateAttr from pyatlan.client.atlan import AtlanClient -from pyatlan.pkg.models import CustomPackage +from pyatlan.pkg.models import CustomPackage, RuntimeConfig LOGGER = logging.getLogger(__name__) @@ -47,13 +47,14 @@ def create_templates(self): script.write(content) -def set_client(impersonate_user_id: str) -> AtlanClient: +def get_client(impersonate_user_id: str) -> AtlanClient: """ Set up the default Atlan client, based on environment variables. This will use an API token if found in ATLAN_API_KEY, and will fallback to attempting to impersonate a user if ATLAN_API_KEY is empty. :param impersonate_user_id: unique identifier (GUID) of a user or API token to impersonate + :returns: an initialized client """ base_url = os.environ.get("ATLAN_BASE_URL", "INTERNAL") api_token = os.environ.get("ATLAN_API_KEY", "") @@ -72,3 +73,19 @@ def set_client(impersonate_user_id: str) -> AtlanClient: ) api_key = AtlanClient(base_url=base_url, api_key="").impersonate.escolate() return AtlanClient(base_url=base_url, api_key=api_key) + + +def set_package_ops(run_time_config: RuntimeConfig) -> AtlanClient: + client = get_client(run_time_config.user_id or "") + if run_time_config.agent == "workflow": + headers: dict[str, str] = {} + if run_time_config.agent: + headers["x-atlan-agent"] = run_time_config.agent + if run_time_config.agent_pkg: + headers["x-atlan-agent-package-name"] = run_time_config.agent_pkg + if run_time_config.agent_wfl: + headers["x-atlan-agent-workflow-id"] = run_time_config.agent_wfl + if run_time_config.agent_id: + headers["x-atlan-agent-id"] = run_time_config.agent_id + client.update_headers(headers) + return client From bb8dbc0be5e014b6614a0ee2ad67ff64ac626f15 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Mon, 15 Jan 2024 18:41:06 +0300 Subject: [PATCH 47/56] Correct unit tests --- pyatlan/__init__.py | 7 ++- pyatlan/logging.conf | 4 +- pyatlan/pkg/models.py | 7 +++ pyatlan/pkg/templates/logging_conf.jinja2 | 48 +++++++++++++++++++++ pyatlan/pkg/templates/package_config.jinja2 | 11 +++-- pyatlan/pkg/utils.py | 6 +++ tests/unit/pkg/test_models.py | 1 + 7 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 pyatlan/pkg/templates/logging_conf.jinja2 diff --git a/pyatlan/__init__.py b/pyatlan/__init__.py index 49eeb1497..d71b35d95 100644 --- a/pyatlan/__init__.py +++ b/pyatlan/__init__.py @@ -1,13 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2022 Atlan Pte. Ltd. import logging.config -from importlib import resources +import os from pyatlan.utils import REQUEST_ID_FILTER -if resources.is_resource("pyatlan", "logging.conf"): - with resources.open_text("pyatlan", "logging.conf") as logging_conf: - logging.config.fileConfig(logging_conf) +if os.path.exists("logging.conf"): + logging.config.fileConfig("logging.conf") LOGGER = logging.getLogger(__name__) for handler in LOGGER.handlers: handler.addFilter(REQUEST_ID_FILTER) diff --git a/pyatlan/logging.conf b/pyatlan/logging.conf index 710c803d2..ac5e9de44 100644 --- a/pyatlan/logging.conf +++ b/pyatlan/logging.conf @@ -19,7 +19,7 @@ propagate=0 [logger_urllib3] level=DEBUG -handlers=consoleHandler,fileHandler,jsonHandler +handlers=fileHandler,jsonHandler qualname=urllib3 propagate=0 @@ -32,7 +32,7 @@ args=(sys.stdout,) class=FileHandler level=DEBUG formatter=simpleFormatter -args=('/tmp/debug.log',) +args=('/tmp/pyatlan.log',) [handler_jsonHandler] class=FileHandler diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index 2b27fcd7b..c998792be 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -280,6 +280,12 @@ def create_config_class(self): ).open("w") as script: script.write(content) + def create_logging_conf(self): + template = self._env.get_template("logging_conf.jinja2") + content = template.render({"pkg": self.pkg}) + with (self.path / "logging.conf").open("w") as script: + script.write(content) + @validate_arguments() def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "config"]): @@ -288,3 +294,4 @@ def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "conf writer.create_package() else: writer.create_config_class() + writer.create_logging_conf() diff --git a/pyatlan/pkg/templates/logging_conf.jinja2 b/pyatlan/pkg/templates/logging_conf.jinja2 new file mode 100644 index 000000000..b30f77526 --- /dev/null +++ b/pyatlan/pkg/templates/logging_conf.jinja2 @@ -0,0 +1,48 @@ +[loggers] +keys=root,pyatlan,urllib3 + +[handlers] +keys=consoleHandler,fileHandler,jsonHandler + +[formatters] +keys=simpleFormatter,jsonFormatter + +[logger_root] +level=INFO +handlers=consoleHandler + +[logger_pyatlan] +level=DEBUG +handlers=fileHandler,jsonHandler +qualname=pyatlan +propagate=0 + +[logger_urllib3] +level=DEBUG +handlers=fileHandler,jsonHandler +qualname=urllib3 +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_fileHandler] +class=FileHandler +level=DEBUG +formatter=simpleFormatter +args=('/tmp/debug.log',) + +[handler_jsonHandler] +class=FileHandler +level=DEBUG +formatter=jsonFormatter +args=('/tmp/pyatlan.json',) + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s + +[formatter_jsonFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +class=pyatlan.utils.JsonFormatter diff --git a/pyatlan/pkg/templates/package_config.jinja2 b/pyatlan/pkg/templates/package_config.jinja2 index 4960328b5..04817d639 100644 --- a/pyatlan/pkg/templates/package_config.jinja2 +++ b/pyatlan/pkg/templates/package_config.jinja2 @@ -1,4 +1,5 @@ from datetime import datetime +from pathlib import Path from pydantic import BaseModel, BaseSettings, Field, parse_obj_as, validator from pyatlan.model.assets import Connection from pyatlan.model.enums import AtlanConnectorType @@ -7,9 +8,11 @@ import json import logging.config import os -if resources.is_resource("pyatlan", "logging.conf"): - with resources.open_text("ernest", "logging.conf") as logging_conf: - logging.config.fileConfig(logging_conf) +PARENT = Path(__file__).parent +LOGGING_CONF = PARENT / "logging.conf" +print("LOGGING_CONF.exists():", LOGGING_CONF.exists()) +if LOGGING_CONF.exists(): + logging.config.fileConfig(LOGGING_CONF) LOGGER = logging.getLogger(__name__) ENV = 'env' @@ -67,7 +70,7 @@ class RuntimeConfig(BaseSettings): }, 'agent_wfl': { ENV: 'X_ATLAN_AGENT_WORKFLOW_ID' - } + }, 'custom_config': { ENV: 'NESTED_CONFIG' } diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index 77924cd59..7eb202319 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -76,6 +76,12 @@ def get_client(impersonate_user_id: str) -> AtlanClient: def set_package_ops(run_time_config: RuntimeConfig) -> AtlanClient: + """ + Set up and processing options and configure the AtlanClient + + :param run_time_config: the generated RuntimeConfig from the generated config module + :returns: an intialized AtlanClient that should be used for any calls to the SDK + """ client = get_client(run_time_config.user_id or "") if run_time_config.agent == "workflow": headers: dict[str, str] = {} diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py index 01e36d162..030c06ff3 100644 --- a/tests/unit/pkg/test_models.py +++ b/tests/unit/pkg/test_models.py @@ -198,3 +198,4 @@ def test_generate_with_operation_config(mock_package_writer, custom_package): generate(pkg=custom_package, path="..", operation="config") mock_package_writer.create_config_class.assert_called() + mock_package_writer.create_logging_conf.assert_called() From a045940792c2be7bace53457aae35bc86918b7ac Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Mon, 15 Jan 2024 19:05:58 +0300 Subject: [PATCH 48/56] Update version --- pyatlan/pkg/models.py | 5 ++++- pyatlan/version.txt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index c998792be..bce3d1b32 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -4,6 +4,7 @@ import logging import textwrap from enum import Enum +from importlib import resources from pathlib import Path from typing import Literal, Optional, Protocol @@ -15,6 +16,8 @@ LOGGER = logging.getLogger(__name__) +VERSION = resources.read_text("pyatlan", "version.txt").strip() + class RuntimeConfig(Protocol): user_id: Optional[str] @@ -103,7 +106,7 @@ def __init__(self, **data): ) self._package_definition = _PackageDefinition( name=self.package_id, - version="1.9.0-SNAPSHOT", + version=VERSION, description=self.description, keywords=self.keywords, homepage=f"https://packages.atlan.com/-/web/detail/{self.package_id}", diff --git a/pyatlan/version.txt b/pyatlan/version.txt index d327595aa..8decb929b 100644 --- a/pyatlan/version.txt +++ b/pyatlan/version.txt @@ -1 +1 @@ -1.8.5-a.1 +1.8.5 From 394199904c87aca2eb5be97d8756914664b22cb6 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 10:17:06 +0300 Subject: [PATCH 49/56] Made changes per code review --- pyatlan/pkg/models.py | 16 -------------- pyatlan/pkg/utils.py | 40 +--------------------------------- pyatlan/pkg/widgets.py | 6 +---- pyatlan/version.txt | 2 +- tests/unit/pkg/test_widgets.py | 4 ++-- 5 files changed, 5 insertions(+), 63 deletions(-) diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index bce3d1b32..f3fd88fc6 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -217,20 +217,6 @@ def indexJS() -> str: """ ) - @staticmethod - def create_package(pkg: "CustomPackage", args: list[str]): - path = args[0] - root_dir = Path(path) / pkg.name - root_dir.mkdir(parents=True, exist_ok=True) - with (root_dir / "index.js").open("w") as index: - index.write(CustomPackage.indexJS()) - with (root_dir / "package.json").open("w") as package: - package.write(pkg.packageJSON) - config_maps_dir = root_dir / "configmaps" - config_maps_dir.mkdir(parents=True, exist_ok=True) - templates_dir = root_dir / "templates" - templates_dir.mkdir(parents=True, exist_ok=True) - class PackageWriter(BaseModel): path: Path @@ -252,8 +238,6 @@ def __init__(self, **data): def create_package(self): self._root_dir.mkdir(parents=True, exist_ok=True) - with (self._root_dir / "index.js").open("w") as index: - index.write(CustomPackage.indexJS()) with (self._root_dir / "index.js").open("w") as index: index.write(CustomPackage.indexJS()) with (self._root_dir / "package.json").open("w") as package: diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index 7eb202319..1526f313b 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -2,51 +2,13 @@ # Copyright 2023 Atlan Pte. Ltd. import logging import os -from pathlib import Path - -from jinja2 import Environment, PackageLoader -from pydantic import BaseModel, PrivateAttr from pyatlan.client.atlan import AtlanClient -from pyatlan.pkg.models import CustomPackage, RuntimeConfig +from pyatlan.pkg.models import RuntimeConfig LOGGER = logging.getLogger(__name__) -class PackageWriter(BaseModel): - path: str - pkg: CustomPackage - _root_dir: Path = PrivateAttr() - _config_maps_dir: Path = PrivateAttr() - _templates_dir: Path = PrivateAttr() - _env: Environment = PrivateAttr() - - def __init__(self, **data): - super().__init__(**data) - self._root_dir = Path(self.path) / self.pkg.name - self._config_maps_dir = self._root_dir / "configmaps" - self._templates_dir = self._root_dir / "templates" - self._env = Environment( # noqa: S701 - loader=PackageLoader("pyatlan.pkg", "templates") - ) - - def create_package(self): - self._root_dir.mkdir(parents=True, exist_ok=True) - with (self._root_dir / "index.js").open("w") as index: - index.write(CustomPackage.indexJS()) - with (self._root_dir / "index.js").open("w") as index: - index.write(CustomPackage.indexJS()) - with (self._root_dir / "package.json").open("w") as package: - package.write(self.pkg.packageJSON) - - def create_templates(self): - self._templates_dir.mkdir(parents=True, exist_ok=True) - template = self._env.get_template("default_template.jinja2") - content = template.render({"pkg": self.pkg}) - with (self._templates_dir / "default.yaml").open("w") as script: - script.write(content) - - def get_client(impersonate_user_id: str) -> AtlanClient: """ Set up the default Atlan client, based on environment variables. diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index cf723ed99..35677c327 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -3,8 +3,6 @@ import abc import json from dataclasses import dataclass, field - -# from dataclasses import dataclass from typing import Optional, Union from pydantic import ( @@ -17,8 +15,6 @@ ) from pydantic.json import pydantic_encoder -# from pydantic.dataclasses import dataclass - Widget = Union[ "APITokenSelectorWidget", "BooleanInputWidget", @@ -687,7 +683,7 @@ def __init__( class MultipleUsersWidget(AbstractWidget): def __init__(self, label: str, hidden: bool = False, help: str = "", grid: int = 8): super().__init__( - widget="groupMultiple", + widget="userMultiple", label=label, hidden=hidden, help=help, diff --git a/pyatlan/version.txt b/pyatlan/version.txt index 8decb929b..f8e233b27 100644 --- a/pyatlan/version.txt +++ b/pyatlan/version.txt @@ -1 +1 @@ -1.8.5 +1.9.0 diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index 5182b636b..f18d3bbc8 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -1212,7 +1212,7 @@ def test_constructor_with_defaults(self): ui = sut.ui assert ui assert isinstance(ui, MultipleUsersWidget) - assert ui.widget == "groupMultiple" + assert ui.widget == "userMultiple" assert ui.label == LABEL assert ui.hidden == IS_NOT_HIDDEN assert ui.help == "" @@ -1232,7 +1232,7 @@ def test_constructor_with_overrides(self): ui = sut.ui assert ui assert isinstance(ui, MultipleUsersWidget) - assert ui.widget == "groupMultiple" + assert ui.widget == "userMultiple" assert ui.label == LABEL assert ui.hidden == IS_HIDDEN assert ui.help == HELP From 0abb6ae43a250f203f18d3ef40e974b5e4d4051e Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 10:34:32 +0300 Subject: [PATCH 50/56] Update History --- HISTORY.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index ca948da84..aefce2eb9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,16 @@ +## 1.9.0 (January 16, 2024) + +### New Features + +- Add ability to update certificate, announcement for GlossaryTerm and GlossaryCategory +- Add `create` method for `ColumnProcess` +- Always include sort by `GUID` as final criteria in `IndexSearch` +- (Experimental) Add classes to support custom package generation + +### QOL Improvements + +- add an additional parameter to `create` method of `ADLSObject` + ## 1.8.4 (January 4, 2024) ### New Features From 49cac12044fcc17ea340fb896360b3c276cf9112 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 13:29:35 +0300 Subject: [PATCH 51/56] Fix integration test --- pyatlan/pkg/models.py | 7 +- pyatlan/pkg/templates/package_config.jinja2 | 6 -- tests/integration/custom_package_test.py | 51 +++++++++++-- tests/integration/logging.conf | 48 ++++++++++++ tests/integration/owner_propagator_cfg.py | 81 +++++++++++++++++++++ tests/unit/pkg/test_models.py | 3 +- 6 files changed, 179 insertions(+), 17 deletions(-) create mode 100644 tests/integration/logging.conf create mode 100644 tests/integration/owner_propagator_cfg.py diff --git a/pyatlan/pkg/models.py b/pyatlan/pkg/models.py index f3fd88fc6..7d35c2af6 100644 --- a/pyatlan/pkg/models.py +++ b/pyatlan/pkg/models.py @@ -245,6 +245,10 @@ def create_package(self): self.create_templates() self.create_configmaps() + def create_config(self): + self.create_config_class() + self.create_logging_conf() + def create_templates(self): self._templates_dir.mkdir(parents=True, exist_ok=True) template = self._env.get_template("default_template.jinja2") @@ -280,5 +284,4 @@ def generate(pkg: CustomPackage, path: Path, operation: Literal["package", "conf if operation == "package": writer.create_package() else: - writer.create_config_class() - writer.create_logging_conf() + writer.create_config() diff --git a/pyatlan/pkg/templates/package_config.jinja2 b/pyatlan/pkg/templates/package_config.jinja2 index 04817d639..428f63fbe 100644 --- a/pyatlan/pkg/templates/package_config.jinja2 +++ b/pyatlan/pkg/templates/package_config.jinja2 @@ -10,7 +10,6 @@ import os PARENT = Path(__file__).parent LOGGING_CONF = PARENT / "logging.conf" -print("LOGGING_CONF.exists():", LOGGING_CONF.exists()) if LOGGING_CONF.exists(): logging.config.fileConfig(LOGGING_CONF) LOGGER = logging.getLogger(__name__) @@ -80,8 +79,3 @@ class RuntimeConfig(BaseSettings): if field_name == 'custom_config': return CustomConfig.parse_raw(raw_value) return cls.json_loads(raw_value) - -if __name__ == "__main__": - LOGGER.info(os.environ["NESTED_CONFIG"]) - r = RuntimeConfig() - LOGGER.info(r.json()) diff --git a/tests/integration/custom_package_test.py b/tests/integration/custom_package_test.py index dbfbe2517..042f33f91 100644 --- a/tests/integration/custom_package_test.py +++ b/tests/integration/custom_package_test.py @@ -1,11 +1,16 @@ -from pyatlan.pkg.models import CustomPackage +import importlib.util +from pathlib import Path + +import pytest + +from pyatlan.pkg.models import CustomPackage, generate from pyatlan.pkg.ui import UIConfig, UIStep -from pyatlan.pkg.utils import PackageWriter from pyatlan.pkg.widgets import TextInput -def test_custom_package(): - pkg = CustomPackage( +@pytest.fixture +def custom_package(): + return CustomPackage( package_id="@csa/owner-propagator", package_name="Owner Propagator", description="Propagate owners from schema downwards.", @@ -31,6 +36,38 @@ def test_custom_package(): container_image="ghcr.io/atlanhq/csa-owner-propagator:123", container_command=["/dumb-init", "--", "java", "OwnerPropagator"], ) - writer = PackageWriter(path="../../generated_packages", pkg=pkg) - writer.create_package() - writer.create_templates() + + +def test_generate_package(custom_package: CustomPackage, tmpdir): + dir = Path(tmpdir.mkdir("generated_packages")) + + generate(pkg=custom_package, path=dir, operation="package") + + package_dir = dir / "csa-owner-propagator" + assert package_dir.exists() + assert (package_dir / "index.js").exists() + assert (package_dir / "package.json").exists() + configmaps_dir = package_dir / "configmaps" + assert configmaps_dir.exists() + assert (configmaps_dir / "default.yaml").exists() + templates_dir = package_dir / "templates" + assert templates_dir.exists() + assert (templates_dir / "default.yaml").exists() + + +def test_generate_config(custom_package: CustomPackage, tmpdir): + dir = Path(tmpdir) + + generate(pkg=custom_package, path=dir, operation="config") + + assert dir / "logging.conf" + config_name = "owner_propagator_cfg.py" + assert dir / config_name + + spec = importlib.util.spec_from_file_location( + "owner_propagator_cfg", dir / config_name + ) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert module is not None + spec.loader.exec_module(module) diff --git a/tests/integration/logging.conf b/tests/integration/logging.conf new file mode 100644 index 000000000..b30f77526 --- /dev/null +++ b/tests/integration/logging.conf @@ -0,0 +1,48 @@ +[loggers] +keys=root,pyatlan,urllib3 + +[handlers] +keys=consoleHandler,fileHandler,jsonHandler + +[formatters] +keys=simpleFormatter,jsonFormatter + +[logger_root] +level=INFO +handlers=consoleHandler + +[logger_pyatlan] +level=DEBUG +handlers=fileHandler,jsonHandler +qualname=pyatlan +propagate=0 + +[logger_urllib3] +level=DEBUG +handlers=fileHandler,jsonHandler +qualname=urllib3 +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_fileHandler] +class=FileHandler +level=DEBUG +formatter=simpleFormatter +args=('/tmp/debug.log',) + +[handler_jsonHandler] +class=FileHandler +level=DEBUG +formatter=jsonFormatter +args=('/tmp/pyatlan.json',) + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s + +[formatter_jsonFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +class=pyatlan.utils.JsonFormatter diff --git a/tests/integration/owner_propagator_cfg.py b/tests/integration/owner_propagator_cfg.py new file mode 100644 index 000000000..bb5c3e9e4 --- /dev/null +++ b/tests/integration/owner_propagator_cfg.py @@ -0,0 +1,81 @@ +import json +import logging.config +import os +from pathlib import Path +from typing import Any, Optional + +from pydantic import BaseModel, BaseSettings, Field, parse_obj_as + +from pyatlan.model.assets import Connection +from pyatlan.model.enums import AtlanConnectorType + +PARENT = Path(__file__).parent +LOGGING_CONF = PARENT / "logging.conf" +print("LOGGING_CONF.exists():", LOGGING_CONF.exists()) +if LOGGING_CONF.exists(): + logging.config.fileConfig(LOGGING_CONF) +LOGGER = logging.getLogger(__name__) + +ENV = "env" + + +def validate_multiselect(v, values, **kwargs): + if isinstance(v, str): + if v.startswith("["): + data = json.loads(v) + v = parse_obj_as(list[str], data) + else: + v = [v] + return v + + +def validate_connection(v, values, config, field, **kwargs): + v = Connection.parse_raw(v) + + +class ConnectorAndConnection(BaseModel): + source: AtlanConnectorType + connections: list[str] + + +def validate_connector_and_connection(v, values, config, field, **kwargs): + return ConnectorAndConnection.parse_raw(v) + + +class CustomConfig(BaseModel): + """""" "" + + qn_prefix: str + + +class RuntimeConfig(BaseSettings): + user_id: Optional[str] = Field(default="") + agent: Optional[str] = Field(default="") + agent_id: Optional[str] = Field(default="") + agent_pkg: Optional[str] = Field(default="") + agent_wfl: Optional[str] = Field(default="") + custom_config: Optional[CustomConfig] = None + + class Config: + fields = { + "user_id": { + ENV: "ATLAN_USER_ID", + }, + "agent": {ENV: "X_ATLAN_AGENT"}, + "agent_id": {ENV: "X_ATLAN_AGENT_ID"}, + "agent_pkg": {ENV: "X_ATLAN_AGENT_PACKAGE_NAME"}, + "agent_wfl": {ENV: "X_ATLAN_AGENT_WORKFLOW_ID"}, + "custom_config": {ENV: "NESTED_CONFIG"}, + } + + @classmethod + def parse_env_var(cls, field_name: str, raw_value: str) -> Any: + if field_name == "custom_config": + return CustomConfig.parse_raw(raw_value) + return cls.json_loads(raw_value) + + +if __name__ == "__main__": + LOGGER.info(os.environ["NESTED_CONFIG"]) + r = RuntimeConfig() + LOGGER.info(r.json()) diff --git a/tests/unit/pkg/test_models.py b/tests/unit/pkg/test_models.py index 030c06ff3..3fbef4e54 100644 --- a/tests/unit/pkg/test_models.py +++ b/tests/unit/pkg/test_models.py @@ -197,5 +197,4 @@ def test_generate_with_operation_package(mock_package_writer, custom_package): def test_generate_with_operation_config(mock_package_writer, custom_package): generate(pkg=custom_package, path="..", operation="config") - mock_package_writer.create_config_class.assert_called() - mock_package_writer.create_logging_conf.assert_called() + mock_package_writer.create_config.assert_called() From fac42afc698d1678f488369a09e77e9a500dbe0f Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 14:03:34 +0300 Subject: [PATCH 52/56] Add new environment variables --- pyatlan/pkg/templates/default_template.jinja2 | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pyatlan/pkg/templates/default_template.jinja2 b/pyatlan/pkg/templates/default_template.jinja2 index f86a3e417..6ea7ee37b 100644 --- a/pyatlan/pkg/templates/default_template.jinja2 +++ b/pyatlan/pkg/templates/default_template.jinja2 @@ -61,6 +61,16 @@ spec: value: "{% raw %}{{=sprig.dig('annotations', 'package', 'argoproj', 'io/name', '', workflow)}}{% endraw %}" - name: X_ATLAN_AGENT_WORKFLOW_ID value: "{% raw %}{{=sprig.dig('labels', 'workflows', 'argoproj', 'io/workflow-template', '', workflow)}}{% endraw %}" + - name: AWS_S3_BUCKET_NAME + valueFrom: + configMapKeyRef: + key: bucket + name: atlan-defaults + - name: AWS_S3_REGION + valueFrom: + configMapKeyRef: + key: region + name: atlan-defaults - name: CLIENT_ID valueFrom: secretKeyRef: @@ -71,6 +81,31 @@ spec: secretKeyRef: name: argo-client-creds key: password + - name: SMTP_HOST + valueFrom: + secretKeyRef: + key: host + name: support-smtp-creds + - name: SMTP_PORT + valueFrom: + secretKeyRef: + key: port + name: support-smtp-creds + - name: SMTP_FROM + valueFrom: + secretKeyRef: + key: from + name: support-smtp-creds + - name: SMTP_USER + valueFrom: + secretKeyRef: + key: login + name: support-smtp-creds + - name: SMTP_PASS + valueFrom: + secretKeyRef: + key: smtp_password + name: workflow-parameter-store {%- for name, property in pkg.ui_config.properties.items() %} - name: {{ name | upper }} value: "{{property.ui.to_env(name)}}" From b2c7d6726be12e63de17974f4a623c2d690385ea Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 16:35:06 +0300 Subject: [PATCH 53/56] Fix problem with incorrect rule JSON generation --- pyatlan/pkg/templates/default_configmap.jinja2 | 2 +- pyatlan/pkg/ui.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyatlan/pkg/templates/default_configmap.jinja2 b/pyatlan/pkg/templates/default_configmap.jinja2 index 0cf0dbc75..64a8fa5db 100644 --- a/pyatlan/pkg/templates/default_configmap.jinja2 +++ b/pyatlan/pkg/templates/default_configmap.jinja2 @@ -20,7 +20,7 @@ data: }, "anyOf": [ {%- for rule in pkg.ui_config.rules %} - {{ rule.to_json() | indent(10) }} + {{ rule.to_json() | indent(10) }}{%- if not loop.last %},{%- endif %} {%- endfor %} ], "steps": [ diff --git a/pyatlan/pkg/ui.py b/pyatlan/pkg/ui.py index 0a567c2d5..549967837 100644 --- a/pyatlan/pkg/ui.py +++ b/pyatlan/pkg/ui.py @@ -115,7 +115,11 @@ def __init__( self.properties = {key: {"const": value} for key, value in when_inputs.items()} def to_json(self: TUIRule) -> str: - return json.dumps(self.properties, indent=2, default=pydantic_encoder) + return json.dumps( + {"properties": self.properties, "required": self.required}, + indent=2, + default=pydantic_encoder, + ) @dataclass() From 0daf0f0a359373c6c8867eecf6ad0319f76ebfe1 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 17:42:44 +0300 Subject: [PATCH 54/56] Renamed file_types to accept in FileUploaderWidget --- pyatlan/pkg/widgets.py | 8 ++++---- tests/unit/pkg/test_widgets.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyatlan/pkg/widgets.py b/pyatlan/pkg/widgets.py index 35677c327..7f5d79bb3 100644 --- a/pyatlan/pkg/widgets.py +++ b/pyatlan/pkg/widgets.py @@ -516,12 +516,12 @@ def __init__( @dataclasses.dataclass class FileUploaderWidget(AbstractWidget): - file_types: list[str] = field(default_factory=list) + accept: list[str] = field(default_factory=list) def __init__( self, label: str, - file_types: list[str], + accept: list[str], hidden: bool = False, help: str = "", placeholder: str = "", @@ -533,7 +533,7 @@ def __init__( help=help, placeholder=placeholder, ) - self.file_types = file_types + self.accept = accept def to_nested(self, name: str) -> str: return f'"/tmp/{name}/{{{{inputs.parameters.{name}}}}}"' # noqa: S108 @@ -575,7 +575,7 @@ def __init__( """ widget = FileUploaderWidget( label=label, - file_types=file_types, + accept=file_types, hidden=hidden, help=help, placeholder=placeholder, diff --git a/tests/unit/pkg/test_widgets.py b/tests/unit/pkg/test_widgets.py index f18d3bbc8..794cb1f10 100644 --- a/tests/unit/pkg/test_widgets.py +++ b/tests/unit/pkg/test_widgets.py @@ -915,7 +915,7 @@ def test_constructor_with_defaults(self): assert isinstance(ui, FileUploaderWidget) assert ui.widget == "fileUpload" assert ui.label == LABEL - assert ui.file_types == FILE_TYPES + assert ui.accept == FILE_TYPES assert ui.hidden == IS_NOT_HIDDEN assert ui.help == "" assert ui.placeholder == "" @@ -937,7 +937,7 @@ def test_constructor_with_overrides(self): assert isinstance(ui, FileUploaderWidget) assert ui.widget == "fileUpload" assert ui.label == LABEL - assert ui.file_types == FILE_TYPES + assert ui.accept == FILE_TYPES assert ui.hidden == IS_HIDDEN assert ui.help == HELP assert ui.placeholder == PLACE_HOLDER From d8add9d64c9adfa70c46843d70cd7f49776dc543 Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 19:01:04 +0300 Subject: [PATCH 55/56] Fix default property --- pyatlan/pkg/templates/default_configmap.jinja2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyatlan/pkg/templates/default_configmap.jinja2 b/pyatlan/pkg/templates/default_configmap.jinja2 index 64a8fa5db..dcc15e832 100644 --- a/pyatlan/pkg/templates/default_configmap.jinja2 +++ b/pyatlan/pkg/templates/default_configmap.jinja2 @@ -13,6 +13,9 @@ data: {%- if value.possible_values %} "enum": [ {%- for val in value.possible_values.keys() %}{{ " " }}"{{ val }}"{%- if not loop.last %},{%- endif %}{% endfor -%}{{ " ]" }}, "enumNames": [ {%- for val in value.possible_values.values() %}{{ " " }}"{{ val }}"{%- if not loop.last %},{%- endif %}{% endfor -%}{{ " ]" }}, + {%- if value.default %} + "default": "{{ value.default }}", + {%- endif %} {%- endif %} "ui": {{ value.ui.to_json() | indent(10)}} }{%- if not loop.last %},{%- endif %} From 94ed66f6eaae48eccb2a43abdae69f25299dedcb Mon Sep 17 00:00:00 2001 From: Ernest Hill Date: Tue, 16 Jan 2024 19:04:10 +0300 Subject: [PATCH 56/56] Make change per code review --- pyatlan/client/impersonate.py | 2 +- pyatlan/pkg/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyatlan/client/impersonate.py b/pyatlan/client/impersonate.py index 852cefb68..51a6aca33 100644 --- a/pyatlan/client/impersonate.py +++ b/pyatlan/client/impersonate.py @@ -74,7 +74,7 @@ def _get_client_info(self) -> ClientInfo: client_info = ClientInfo(client_id=client_id, client_secret=client_secret) return client_info - def escolate(self) -> str: + def escalate(self) -> str: """ Escalate to a privileged user on a short-term basis. Note: this is only possible from within the Atlan tenant, and only when given the appropriate credentials. diff --git a/pyatlan/pkg/utils.py b/pyatlan/pkg/utils.py index 1526f313b..6711136d9 100644 --- a/pyatlan/pkg/utils.py +++ b/pyatlan/pkg/utils.py @@ -33,7 +33,7 @@ def get_client(impersonate_user_id: str) -> AtlanClient: LOGGER.info( "No API token or impersonation user, attempting short-lived escalation." ) - api_key = AtlanClient(base_url=base_url, api_key="").impersonate.escolate() + api_key = AtlanClient(base_url=base_url, api_key="").impersonate.escalate() return AtlanClient(base_url=base_url, api_key=api_key)