Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 45 additions & 58 deletions connect_transformations/currency_conversion/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,41 @@
# All rights reserved.
#
from decimal import Decimal
from functools import cached_property
from typing import List

import httpx
import requests
from connect.eaas.core.decorators import router, transformation
from connect.eaas.core.responses import RowTransformationResponse

from connect_transformations.currency_conversion.exceptions import CurrencyConversionError
from connect_transformations.currency_conversion.models import Configuration, Currency
from connect_transformations.currency_conversion.utils import validate_currency_conversion
from connect_transformations.currency_conversion.utils import (
load_currency_rate,
validate_currency_conversion,
)
from connect_transformations.models import Error, ValidationResult
from connect_transformations.utils import is_input_column_nullable


class CurrencyConverterTransformationMixin:

def preload_currency_conversion_rates(self, currency_from, currency_to):
with self.lock():
if hasattr(self, 'currency_conversion_rates'):
return
def get_currency_rate(self, currency_from, currency_to):
if not hasattr(self, '_currency_rates'):
self._currency_rates = {}

self.currency_conversion_rates = {}
key = (currency_from, currency_to)

try:
url = 'https://api.exchangerate.host/latest'
params = {
'symbols': currency_to,
'base': currency_from,
}

response = requests.get(
url,
params=params,
)
response.raise_for_status()
data = response.json()

if not data['success']:
raise CurrencyConversionError(
f'Unexpected response calling {url}'
f' with params {params}',
)
self.currency_conversion_rates[currency_to] = Decimal(data['rates'][currency_to])
except requests.RequestException as exc:
raise CurrencyConversionError(
f'An error occurred while requesting {url} with '
f'params {params}: {exc}',
)
if key not in self._currency_rates:
with self.lock():
if key not in self._currency_rates:
self._currency_rates[key] = load_currency_rate(currency_from, currency_to)

return self._currency_rates[key]

@cached_property
def input_columns(self):
input_columns = self.transformation_request['transformation']['columns']['input']
return {c['id']: c for c in input_columns}

@transformation(
name='Convert Currency',
Expand All @@ -65,32 +52,32 @@ def currency_conversion(
self,
row,
):
trfn_settings = self.transformation_request['transformation']['settings']
value = row[trfn_settings['from']['column']]
currency = trfn_settings['from']['currency']
currency_to = trfn_settings['to']['currency']
return_values = {}
settings = self.transformation_request['transformation']['settings']
if not isinstance(settings, (tuple, list)):
settings = [settings]

for conv_settings in settings:
col_name = self.input_columns[conv_settings['from']['column']]['name']
value = row[col_name]
currency_from = conv_settings['from']['currency']
currency_to = conv_settings['to']['currency']

if (not value) and is_input_column_nullable(
self.transformation_request['transformation']['columns']['input'],
col_name,
):
return RowTransformationResponse.skip()

try:
self.preload_currency_conversion_rates(
currency,
currency_to,
)
except Exception as e:
return RowTransformationResponse.fail(output=str(e))

if is_input_column_nullable(
self.transformation_request['transformation']['columns']['input'],
trfn_settings['from']['column'],
) and not value:
return RowTransformationResponse.skip()

return RowTransformationResponse.done({
trfn_settings['to']['column']: (
Decimal(value) * self.currency_conversion_rates[currency_to]
).quantize(
Decimal('.00001'),
),
})
try:
return_values[conv_settings['to']['column']] = (
Decimal(value) * self.get_currency_rate(currency_from, currency_to)
).quantize(
Decimal('.00001'),
)
except Exception as e:
return RowTransformationResponse.fail(output=str(e))
return RowTransformationResponse.done(return_values)


class CurrencyConversionWebAppMixin:
Expand Down
4 changes: 2 additions & 2 deletions connect_transformations/currency_conversion/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import List, Optional

from pydantic import BaseModel

Expand Down Expand Up @@ -26,5 +26,5 @@ class Config:


class Configuration(BaseModel):
settings: Optional[Settings]
settings: Optional[List[Settings]]
columns: Optional[Columns]
109 changes: 79 additions & 30 deletions connect_transformations/currency_conversion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
# Copyright (c) 2023, CloudBlue LLC
# All rights reserved.
#
from decimal import Decimal

import requests

from connect_transformations.currency_conversion.exceptions import CurrencyConversionError
from connect_transformations.utils import (
build_error_response,
does_not_contain_required_keys,
Expand All @@ -13,43 +18,87 @@
def validate_currency_conversion(data):
data = data.dict(by_alias=True)

if has_invalid_basic_structure(data):
if has_invalid_basic_structure(data, data_type=(list, dict)):
return build_error_response('Invalid input data')

trf_settings = data['settings']
if not isinstance(trf_settings, list):
trf_settings = [trf_settings]

input_columns = data['columns']['input']
available_input_columns = [c['name'] for c in input_columns]
if (
does_not_contain_required_keys(
data['settings'],
['from', 'to'],
) or does_not_contain_required_keys(
data['settings']['from'],
['column', 'currency'],
) or does_not_contain_required_keys(
data['settings']['to'],
['column', 'currency'],
)
):
return build_error_response(
'The settings must have `from` with the `column` and the `currency`, and '
'`to` with the `column` and `currency` fields',
)
available_input_columns = {c['id']: c for c in input_columns}

if data['settings']['from']['currency'] == data['settings']['to']['currency']:
return build_error_response(
'The settings must have different currencies for `from` and `to`',
)
overview = ''

if data['settings']['from']['column'] not in available_input_columns:
return build_error_response(
'The settings contains an invalid `from` column name'
f' "{data["settings"]["from"]["column"]}" that does not exist on '
'columns.input',
)
for row in trf_settings:
if (
does_not_contain_required_keys(
row,
['from', 'to'],
) or does_not_contain_required_keys(
row['from'],
['column', 'currency'],
) or does_not_contain_required_keys(
row['to'],
['column', 'currency'],
)
):
return build_error_response(
'The settings must have `from` with the `column` and the `currency`, and '
'`to` with the `column` and `currency` fields',
)

if row['from']['currency'] == row['to']['currency']:
return build_error_response(
'The settings must have different currencies for `from` and `to`',
)

if row['from']['column'] not in available_input_columns:
return build_error_response(
'The settings contains an invalid `from` column name'
f' "{row["from"]["column"]}" that does not exist on '
'columns.input',
)

overview = 'From Currency = ' + data['settings']['from']['currency'] + '\n'
overview += 'To Currency = ' + data['settings']['to']['currency'] + '\n'
if overview:
overview += '\n'

overview += 'From: {src} ({src_curr})\nTo: {dst} ({dst_curr})\n'.format(
src=available_input_columns[row['from']['column']]['name'],
src_curr=row['from']['currency'],
dst=row['to']['column'],
dst_curr=row['to']['currency'],
)

return {
'overview': overview,
}


def load_currency_rate(currency_from, currency_to):
try:
url = 'https://api.exchangerate.host/latest'
params = {
'symbols': currency_to,
'base': currency_from,
}

response = requests.get(
url,
params=params,
)
response.raise_for_status()
data = response.json()

if not data['success']:
raise CurrencyConversionError(
f'Unexpected response calling {url}'
f' with params {params}',
)

return Decimal(data['rates'][currency_to])
except requests.RequestException as exc:
raise CurrencyConversionError(
f'An error occurred while requesting {url} with '
f'params {params}: {exc}',
)
Empty file modified connect_transformations/static/44a9fa4faa4e0d7f2adc.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/5df55ab0f502936aa404.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/67de229ea73f98cf5020.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/6da5e4b6cdf85fa9cd8e.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/bdc8832f2c64c9ce80c5.woff
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/da4f14203f531119a659.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/da615df710c3224b74ab.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/e8f0e86e70b87cc10f1b.woff2
100755 → 100644
Empty file.
Empty file modified connect_transformations/static/fonts/roboto-all-500-normal.woff
100755 → 100644
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file modified connect_transformations/static/images/mkp.svg
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"/><title>Transformations/Airtable Lookup</title><script defer="defer" src="../vendors.b7829ba6a3fa12b6e5cb.js"></script><script defer="defer" src="../transformations/airtable_lookup.378bad986c96d5d867cd.js"></script><link href="../transformations/airtable_lookup.5f5aa442fc131fcaad2c.css" rel="stylesheet"></head><body><div id="loader"></div><div id="app"><div class="main-container"><div class="config-elem api-input"><label for="key-select">API Key</label> <input style="width: 86%;" id="key-input" placeholder="Enter an AirTable key"></div><div class="config-elem"><label for="base-select">AirTable Base</label> <select disabled="disabled" class="list" style="width: 80%;" id="base-select"></select></div><div class="config-elem"><label for="table-select">AirTable table name</label> <select disabled="disabled" class="list" style="width: 80%;" id="table-select"></select></div><div class="config-elem"><label for="table-select">Input column (Key)</label> <select disabled="disabled" class="list" style="width: 80%;" id="input-column-select"></select></div><div class="config-elem"><label for="table-select">AirTable Field Name (Key)</label> <select disabled="disabled" class="list" style="width: 80%;" id="field-select"></select></div></div><b>Columns Mapping</b><div id="content"></div><div class="button-container"><button disabled="disabled" id="add" class="button">ADD COLUMN</button></div></div></body></html>
<!doctype html><html><head><meta charset="utf-8"/><title>Transformations/Airtable Lookup</title><script defer="defer" src="../vendors.b7829ba6a3fa12b6e5cb.js"></script><script defer="defer" src="../transformations/airtable_lookup.378bad986c96d5d867cd.js"></script><link href="../transformations/airtable_lookup.fa2c5fd466f5298cab9e.css" rel="stylesheet"></head><body><div id="loader"></div><div id="app"><div class="main-container"><div class="config-elem api-input"><label for="key-select">API Key</label> <input style="width: 86%;" id="key-input" placeholder="Enter an AirTable key"></div><div class="config-elem"><label for="base-select">AirTable Base</label> <select disabled="disabled" class="list" style="width: 80%;" id="base-select"></select></div><div class="config-elem"><label for="table-select">AirTable table name</label> <select disabled="disabled" class="list" style="width: 80%;" id="table-select"></select></div><div class="config-elem"><label for="table-select">Input column (Key)</label> <select disabled="disabled" class="list" style="width: 80%;" id="input-column-select"></select></div><div class="config-elem"><label for="table-select">AirTable Field Name (Key)</label> <select disabled="disabled" class="list" style="width: 80%;" id="field-select"></select></div></div><b>Columns Mapping</b><div id="content"></div><div class="button-container"><button disabled="disabled" id="add" class="button">ADD COLUMN</button></div></div></body></html>

Large diffs are not rendered by default.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"/><title>Transformations/Attachment Lookup</title><script defer="defer" src="../vendors.b7829ba6a3fa12b6e5cb.js"></script><script defer="defer" src="../transformations/attachment_lookup.5e72f506627cfb05e86b.js"></script><link href="../transformations/attachment_lookup.2c9f2595bae22a0caf51.css" rel="stylesheet"></head><body><div id="loader"></div><div id="app" class="hidden"><div class="main-container" id="content"><div class="row"><div class="col"><div class="input-group"><label class="label" for="attachment">Attachment</label> <select materialize id="attachment"></select></div></div><div class="col"><div class="input-group"><label class="label" for="sheet">Sheet</label> <input materialize id="sheet"></div></div></div><div class="row"><div class="col"><div class="input-group"><label class="label" for="input-column">Input Column (Key)</label> <select materialize id="input-column"></select></div></div><div class="col"><div class="input-group"><label class="label" for="attachment-column">Attachment Field name (Key)</label> <input materialize id="attachment-column"></div></div></div><div id="mapping"><div class="subtitle">Columns mapping</div></div></div></div></body></html>
<!doctype html><html><head><meta charset="utf-8"/><title>Transformations/Attachment Lookup</title><script defer="defer" src="../vendors.b7829ba6a3fa12b6e5cb.js"></script><script defer="defer" src="../transformations/attachment_lookup.5e72f506627cfb05e86b.js"></script><link href="../transformations/attachment_lookup.0775117621fee90b28ad.css" rel="stylesheet"></head><body><div id="loader"></div><div id="app" class="hidden"><div class="main-container" id="content"><div class="row"><div class="col"><div class="input-group"><label class="label" for="attachment">Attachment</label> <select materialize id="attachment"></select></div></div><div class="col"><div class="input-group"><label class="label" for="sheet">Sheet</label> <input materialize id="sheet"></div></div></div><div class="row"><div class="col"><div class="input-group"><label class="label" for="input-column">Input Column (Key)</label> <select materialize id="input-column"></select></div></div><div class="col"><div class="input-group"><label class="label" for="attachment-column">Attachment Field name (Key)</label> <input materialize id="attachment-column"></div></div></div><div id="mapping"><div class="subtitle">Columns mapping</div></div></div></div></body></html>

This file was deleted.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion connect_transformations/static/transformations/copy.html
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"/><title>Transformations/Copy</title><script defer="defer" src="../vendors.b7829ba6a3fa12b6e5cb.js"></script><script defer="defer" src="../transformations/copy.688a33ddba483d0d9c32.js"></script><link href="../transformations/copy.15f999ee385ccc3cdaea.css" rel="stylesheet"></head><body><div id="loader"></div><div id="app" class="application"><div class="main-container" id="content"></div><div class="button-container"><button id="add" class="button">ADD COLUMN</button></div></div></body></html>
<!doctype html><html><head><meta charset="utf-8"/><title>Transformations/Copy</title><script defer="defer" src="../vendors.b7829ba6a3fa12b6e5cb.js"></script><script defer="defer" src="../transformations/copy.688a33ddba483d0d9c32.js"></script><link href="../transformations/copy.a15ed0d89abdd4879a88.css" rel="stylesheet"></head><body><div id="loader"></div><div id="app" class="application"><div class="main-container" id="content"></div><div class="button-container"><button id="add" class="button">ADD COLUMN</button></div></div></body></html>

This file was deleted.

Large diffs are not rendered by default.

Loading