Skip to content

Commit 0dc8ebc

Browse files
authored
Merge pull request #200 from cloudblue/LITE-26686-support-transformations
Lite 26686 support transformations
2 parents e92b2b9 + 64a3011 commit 0dc8ebc

File tree

22 files changed

+1027
-22
lines changed

22 files changed

+1027
-22
lines changed

connect/cli/plugins/project/extension/helpers.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ def bootstrap_extension_project( # noqa: CCR001
9292
if answers['use_github_actions'] == 'n':
9393
exclude.extend(['.github', '.github/**/*'])
9494

95+
application_types = answers.get('application_types', [])
96+
9597
if answers.get('webapp_supports_ui') != 'y':
9698
exclude.extend([
9799
os.path.join('${package_name}', 'static', '.gitkeep'),
@@ -106,9 +108,25 @@ def bootstrap_extension_project( # noqa: CCR001
106108
'jest.config.js.j2',
107109
])
108110

109-
application_types = answers.get('application_types', [])
111+
elif 'tfnapp' not in application_types:
112+
exclude.extend([
113+
'ui/pages/transformations',
114+
'ui/pages/transformations/*',
115+
'ui/src/pages/transformations',
116+
'ui/src/pages/transformations/*',
117+
'ui/styles/manual.css.j2',
118+
])
119+
else:
120+
exclude.extend([
121+
'ui/pages/index.html.j2',
122+
'ui/pages/settings.html.j2',
123+
'ui/src/pages/index.js.j2',
124+
'ui/src/pages/settings.js.j2',
125+
'ui/tests/pages.spec.js.j2',
126+
'ui/tests/utils.spec.js.j2',
127+
])
110128

111-
for app_type in ['anvil', 'events', 'webapp']:
129+
for app_type in ['anvil', 'events', 'webapp', 'tfnapp']:
112130
if app_type not in application_types:
113131
exclude.append(
114132
os.path.join(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) {% now 'utc', '%Y' %}, {{ author }}
4+
# All rights reserved.
5+
#
6+
from connect.eaas.core.decorators import manual_transformation, transformation{% if include_variables_example == 'y' -%}, variables{% endif %}
7+
from connect.eaas.core.extension import TransformationsApplicationBase
8+
9+
10+
{% if include_variables_example == 'y' -%}
11+
@variables([{
12+
'name': 'VAR_NAME_1',
13+
'initial_value': 'VAR_VALUE_1',
14+
'secure': False,
15+
}])
16+
{% endif -%}
17+
class {{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication(TransformationsApplicationBase):
18+
@transformation(
19+
name='Manual transformation',
20+
description=(
21+
'This transformation function allows to describe a manual '
22+
'procedure to be done.'
23+
),
24+
edit_dialog_ui='/static/transformations/manual.html',
25+
)
26+
@manual_transformation()
27+
{% if use_asyncio == 'y' %}async {% endif %}def manual_transformation(self, row: dict):
28+
pass
29+
30+
@transformation(
31+
name='Copy Column(s)',
32+
description=(
33+
'This transformation function allows copy values from Input to Output columns, '
34+
'which can be used in case of change column name in the output data or '
35+
'create a copy of values in table.'
36+
),
37+
edit_dialog_ui='/static/transformations/copy.html',
38+
)
39+
def copy_columns(self, row: dict):
40+
tfn_settings = (
41+
self.transformation_request['transformation']['settings']
42+
)
43+
result = {}
44+
45+
for setting in tfn_settings:
46+
result[setting['to']] = row[setting['from']]
47+
48+
return result
49+

connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/webapp.py.j2

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
# Copyright (c) {% now 'utc', '%Y' %}, {{ author }}
44
# All rights reserved.
55
#
6+
{%- if extension_type == 'transformations' %}
7+
from connect.eaas.core.decorators import (
8+
router,
9+
web_app,
10+
)
11+
from connect.eaas.core.extension import WebApplicationBase
12+
from fastapi.responses import JSONResponse
13+
{%- else %}
614
from typing import List
715

816
from connect.client import {% if use_asyncio == 'y' %}Async{% endif %}ConnectClient{% if extension_type == 'multiaccount' %}, R{% endif %}
917
from connect.eaas.core.decorators import (
10-
{%- if webapp_supports_ui == 'y' %}
18+
{%- if webapp_supports_ui == 'y'%}
1119
account_settings_page,
1220
module_pages,
1321
{%- endif %}
@@ -35,6 +43,7 @@ from connect.eaas.core.inject.synchronous import get_extension_client
3543
from fastapi import Depends
3644

3745
from {{ package_name }}.schemas import Marketplace{% if extension_type == 'multiaccount' %}, Settings{% endif %}
46+
{%- endif %}
3847

3948

4049
{% if include_variables_example == 'y' -%}
@@ -52,13 +61,13 @@ from {{ package_name }}.schemas import Marketplace{% if extension_type == 'multi
5261
])
5362
{% endif -%}
5463
@web_app(router)
55-
{% if webapp_supports_ui == 'y' -%}
64+
{% if webapp_supports_ui == 'y' and extension_type != 'transformations' -%}
5665
@account_settings_page('Chart settings', '/static/settings.html')
5766
@module_pages('Chart', '/static/index.html')
5867
{% endif -%}
5968
class {{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication(WebApplicationBase):
6069

61-
@router.get(
70+
{% if extension_type != 'transformations' -%}@router.get(
6271
'/marketplaces',
6372
summary='List all available marketplaces',
6473
response_model=List[Marketplace],
@@ -77,7 +86,7 @@ class {{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication(
7786
{% if extension_type == 'multiaccount' -%}
7887
@router.get(
7988
'/settings',
80-
summary='Retrive charts settings',
89+
summary='Retrieve charts settings',
8190
response_model=Settings,
8291
)
8392
{% if use_asyncio == 'y' %}async {% endif %}def retrieve_settings(
@@ -132,4 +141,68 @@ class {{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication(
132141
],
133142
},
134143
}
135-
{% endif %}
144+
{%- endif %}
145+
146+
{%- else %}def validate_copy_columns(self, data):
147+
if (
148+
'settings' not in data
149+
or not isinstance(data['settings'], list)
150+
or 'columns' not in data
151+
or 'input' not in data['columns']
152+
):
153+
return JSONResponse(status_code=400, content={'error': 'Invalid input data'})
154+
155+
settings = data['settings']
156+
input_columns = data['columns']['input']
157+
available_input_columns = [c['name'] for c in input_columns]
158+
unique_names = [c['name'] for c in input_columns]
159+
overview = []
160+
161+
for s in settings:
162+
if 'from' not in s or 'to' not in s:
163+
return JSONResponse(
164+
status_code=400,
165+
content={'error': 'Invalid settings format'},
166+
)
167+
if s['from'] not in available_input_columns:
168+
return JSONResponse(
169+
status_code=400,
170+
content={'error': f'The input column {s["from"]} does not exists'},
171+
)
172+
if s['to'] in unique_names:
173+
return JSONResponse(
174+
status_code=400,
175+
content={
176+
'error': f'Invalid column name {s["to"]}. The to field should be unique',
177+
},
178+
)
179+
unique_names.append(s['to'])
180+
overview.append(f'{s["from"]} --> {s["to"]}')
181+
182+
overview = ''.join([row + '\n' for row in overview])
183+
184+
return {
185+
'overview': overview,
186+
}
187+
188+
@router.post(
189+
'/validate/{transformation_function}',
190+
summary='Validate settings',
191+
)
192+
def validate_tfn_settings(
193+
self,
194+
transformation_function: str,
195+
data: dict,
196+
):
197+
try:
198+
method = getattr(self, f'validate_{transformation_function}')
199+
return method(data)
200+
except AttributeError:
201+
return JSONResponse(
202+
status_code=400,
203+
content={
204+
'error': f'The validation method {transformation_function} does not exist',
205+
},
206+
)
207+
208+
{%- endif %}

connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ readme = "./README.md"
1919
{%- if 'anvil' in application_types %}
2020
"anvilapp" = "{{ package_name }}.anvil:{{ project_slug|replace("_", " ")|title|replace(" ", "") }}AnvilApplication"
2121
{%- endif %}
22+
{%- if 'tfnapp' in application_types %}
23+
"tfnapp" = "{{ package_name }}.tfnapp:{{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication"
24+
{%- endif %}
2225

2326
[tool.poetry.dependencies]
2427
python = ">=3.8,<4"
25-
connect-eaas-core = ">=26.13,<27"
28+
connect-eaas-core = ">=27.11,<28"
2629
2730
[tool.poetry.dev-dependencies]
2831
pytest = ">=6.1.2,<8"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) 2022, Globex Corporation
4+
# All rights reserved.
5+
#
6+
from {{ package_name }}.tfnapp import {{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication
7+
8+
9+
def test_copy_columns(connect_client, logger, mocker):
10+
app = {{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication(
11+
connect_client,
12+
logger,
13+
mocker.MagicMock(),
14+
installation_client=connect_client,
15+
installation={'id': 'EIN-0000'},
16+
context=mocker.MagicMock(),
17+
transformation_request={
18+
'transformation': {
19+
'settings': [
20+
{'from': 'ColumnA', 'to': 'NewColC'},
21+
{'from': 'ColumnB', 'to': 'NewColD'},
22+
],
23+
},
24+
},
25+
)
26+
27+
assert app.copy_columns(
28+
{
29+
'ColumnA': 'ContentColumnA',
30+
'ColumnB': 'ContentColumnB',
31+
},
32+
) == {
33+
'NewColC': 'ContentColumnA',
34+
'NewColD': 'ContentColumnB',
35+
}
36+

connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/tests/test_webapp.py.j2

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
# Copyright (c) {% now 'utc', '%Y' %}, {{ author }}
44
# All rights reserved.
55
#
6+
{% if extension_type == 'transformations' -%}
7+
import pytest
8+
9+
{% endif -%}
610
{% if extension_type == 'multiaccount' -%}
711
from connect.client import R
812

@@ -11,6 +15,114 @@ from {{ package_name }}.schemas import Marketplace, Settings
1115
from {{ package_name }}.webapp import {{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication
1216

1317

18+
{% if extension_type == 'transformations' -%}
19+
def test_validate_copy_columns(test_client_factory):
20+
data = {
21+
'settings': [
22+
{
23+
'from': 'columnInput1',
24+
'to': 'newColumn1',
25+
},
26+
{
27+
'from': 'columnInput2',
28+
'to': 'newColumn2',
29+
},
30+
],
31+
'columns': {
32+
'input': [
33+
{'name': 'columnInput1'},
34+
{'name': 'columnInput2'},
35+
],
36+
'output': [],
37+
},
38+
}
39+
40+
client = test_client_factory({{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication)
41+
42+
response = client.post('/api/validate/copy_columns', json=data)
43+
assert response.status_code == 200
44+
45+
data = response.json()
46+
assert data == {
47+
'overview': 'columnInput1 --> newColumn1\ncolumnInput2 --> newColumn2\n',
48+
}
49+
50+
51+
@pytest.mark.parametrize(
52+
'data',
53+
(
54+
{},
55+
{'settings': {}},
56+
{'settings': []},
57+
{'settings': [], 'columns': {}},
58+
),
59+
)
60+
def test_validate_copy_columns_missing_settings_or_invalid(test_client_factory, data):
61+
62+
client = test_client_factory({{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication)
63+
64+
response = client.post('/api/validate/copy_columns', json=data)
65+
assert response.status_code == 400
66+
assert response.json() == {'error': 'Invalid input data'}
67+
68+
69+
def test_validate_copy_columns_invalid_settings(test_client_factory):
70+
data = {'settings': [{'x': 'y'}], 'columns': {'input': []}}
71+
72+
client = test_client_factory({{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication)
73+
74+
response = client.post('/api/validate/copy_columns', json=data)
75+
assert response.status_code == 400
76+
assert response.json() == {'error': 'Invalid settings format'}
77+
78+
79+
def test_validate_copy_columns_invalid_from(test_client_factory):
80+
data = {'settings': [{'from': 'Hola', 'to': 'Hola2'}], 'columns': {'input': [{'name': 'Gola'}]}}
81+
82+
client = test_client_factory({{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication)
83+
84+
response = client.post('/api/validate/copy_columns', json=data)
85+
assert response.status_code == 400
86+
assert response.json() == {'error': 'The input column Hola does not exists'}
87+
88+
89+
@pytest.mark.parametrize(
90+
'data',
91+
(
92+
{
93+
'settings': [
94+
{'from': 'A', 'to': 'C'},
95+
{'from': 'B', 'to': 'C'},
96+
],
97+
'columns': {
98+
'input': [
99+
{'name': 'A'},
100+
{'name': 'B'},
101+
],
102+
},
103+
},
104+
{
105+
'settings': [
106+
{'from': 'A', 'to': 'C'},
107+
],
108+
'columns': {
109+
'input': [
110+
{'name': 'A'},
111+
{'name': 'C'},
112+
],
113+
},
114+
},
115+
),
116+
)
117+
def test_validate_copy_columns_not_unique_name(test_client_factory, data):
118+
client = test_client_factory({{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication)
119+
120+
response = client.post('/api/validate/copy_columns', json=data)
121+
assert response.status_code == 400
122+
assert response.json() == {
123+
'error': 'Invalid column name C. The to field should be unique',
124+
}
125+
{% else -%}
14126
def test_list_marketplaces(test_client_factory, {% if use_asyncio == 'y' %}async_{% endif %}client_mocker_factory):
15127
marketplaces = [
16128
{
@@ -180,3 +292,4 @@ def test_generate_chart_data(test_client_factory, {% if use_asyncio == 'y' %}asy
180292
},
181293
}
182294
{% endif %}
295+
{% endif -%}

0 commit comments

Comments
 (0)