From 4d9546ab8d094273a2b3bdac2d23512973449385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Wed, 28 Feb 2024 13:07:05 +0100 Subject: [PATCH 1/8] ADD initial implementation --- .gitignore | 2 +- README.md | 29 ++++++++++++++++++++++++++++- tcjexl/__init__.py | 18 ++++++++++++++++++ tcjexl/jexl.py | 26 ++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tcjexl/__init__.py create mode 100644 tcjexl/jexl.py diff --git a/.gitignore b/.gitignore index 68bc17f..2dc53ca 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ diff --git a/README.md b/README.md index d1777d8..e9c8ed2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # tcjexl -Recubrimiento de la librería de jexl incluyendo un conjunto de transformaciones por defecto + +Recubrimiento de la librería de [pyjexl](https://pypi.org/project/pyjexl/) incluyendo un conjunto de transformaciones por defecto (detalladas [en esta lista](#funciones-incluida)) + +Ejemplo de uso: + +```python +from tcjexl import JEXL + +jexl = JEXL() + +context = {"a": 5, "b": 7, "c": "a TEXT String"} + +print(jexl.evaluate('a+b', context)) +print(jexl.evaluate('c|lowercase', context)) +``` + +Resultado: + +``` +12 +a text string +``` + +## Funciones incluidas + +* `lowercase`: Transformación que nos convierte un string a lowercase + +## Changelog diff --git a/tcjexl/__init__.py b/tcjexl/__init__.py new file mode 100644 index 0000000..199cab2 --- /dev/null +++ b/tcjexl/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +from tcjexl.jexl import JEXL diff --git a/tcjexl/jexl.py b/tcjexl/jexl.py new file mode 100644 index 0000000..f4897a3 --- /dev/null +++ b/tcjexl/jexl.py @@ -0,0 +1,26 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + + +import pyjexl + + +class JEXL(pyjexl.JEXL): + def __init__(self, context=None): + super().__init__(context=context) + + super().add_transform("lowercase", lambda x: str(x).lower()) From 7fcf35a0b74c89590c06b35de3ad5ea363c3c955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 11:26:53 +0100 Subject: [PATCH 2/8] FIX complete library, including tests and documentation --- README.md | 116 +++++++++++++++++++++++-- setup.py | 53 +++++++++++ tcjexl/__init__.py | 36 ++++---- tcjexl/functions.py | 110 +++++++++++++++++++++++ tcjexl/jexl.py | 104 ++++++++++++++++------ tests/test_basic.py | 30 +++++++ tests/test_transforms_date.py | 82 +++++++++++++++++ tests/test_transforms_interpolation.py | 102 ++++++++++++++++++++++ tests/test_transforms_list.py | 70 +++++++++++++++ tests/test_transforms_math.py | 61 +++++++++++++ tests/test_transforms_misc.py | 51 +++++++++++ tests/test_transforms_string.py | 69 +++++++++++++++ 12 files changed, 835 insertions(+), 49 deletions(-) create mode 100644 setup.py create mode 100644 tcjexl/functions.py create mode 100644 tests/test_basic.py create mode 100644 tests/test_transforms_date.py create mode 100644 tests/test_transforms_interpolation.py create mode 100644 tests/test_transforms_list.py create mode 100644 tests/test_transforms_math.py create mode 100644 tests/test_transforms_misc.py create mode 100644 tests/test_transforms_string.py diff --git a/README.md b/README.md index e9c8ed2..5129adf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # tcjexl -Recubrimiento de la librería de [pyjexl](https://pypi.org/project/pyjexl/) incluyendo un conjunto de transformaciones por defecto (detalladas [en esta lista](#funciones-incluida)) +This is a wrapper of the [pyjexl](https://pypi.org/project/pyjexl/) library, including a set of default transformations (detailed in [this section](#included-transformations)) -Ejemplo de uso: +Example: ```python from tcjexl import JEXL @@ -15,15 +15,121 @@ print(jexl.evaluate('a+b', context)) print(jexl.evaluate('c|lowercase', context)) ``` -Resultado: +Result: ``` 12 a text string ``` -## Funciones incluidas +## Included transformations -* `lowercase`: Transformación que nos convierte un string a lowercase +**NOTE:** JEXL pipeline is needed even if the transformation doesn't need an parameter to work (e.g. `currentTime` that provides the current system time and doesn't need and argument). In this case, we use `0|` for these cases (e.g. `0|currentTime`). + +### Math related transformations + +- `rnd`: returns a random number between two integers. Examples: `0|rnd(10)` returns a random number between 0 (included) and 10 (not included). `12|rnd(99)` returns a random number between 12 (included) and 99 (not included). +- `rndFloat`: returns a random number between two decimal numbers. Examples: `0.2|rndFloat(12.7)` returns a random number between 0.2 and 12.7. +- `round`: rounds a number with a given number of precision digits. Example: `0.12312|round(2)` return 0.12, rounding 0.12312 to two decimals. +- `floor`: rounds a number to the lesser integer. Example: `4.9|floor` returns 4. +- `parseInt`: TBD +- `parseFloat`: TBD + +### String related transformations + +- `uppercase`: converts a given string to uppercase. +- `lowercase`: converts a given string to lowercase. +- `toString`: TBD +- `substring`: TBD +- `includes`: TBD +- `len`: returns the number of items in an array or the length of a string. + +### List related transformations + +- `next`: returns the next item in an array. Example: `12|next([1,2,3,12,15,18])` returns 15. +- `indexOf`: TBD +- `rndList`: returns an array of random elements within two limits. Example: `0|rndList(6,8)` is an array with 8 items and each item is a random number between 0 and 6. +- `rndFloatList`: similar to `rndList`, but with decimal numbers. +- `zipStringList`: concat two arrays of the same length with a given separator. +- `concatList`: concat array elements. + +### Date related transformations + +- `currentTime`: returns current time in UTC format. +- `currentTimeIso`: returns current time in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators) format. +- `toIsoString`: allows to format a date into [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators) format. +- `currentTimeFormat`: allows to format current time with a given format. For instance, if current date is 07/07/2023 and we use `0|currentTimeFormat("%Y")` then `2023` will be returned. +- `timeFormat`: allows to format a given date with a given format. For instance, if current date is 07/07/2023 and we use `0|currentTime|timeFormat("%Y")` then `2023` will be returned. +- `currentHour24`: returns current time in 24 hours format. Example: `0|currentHour24`. +- `currentDay`: returns the current day of the month. Example: `0|currentDay`. +- `schedulerValue`: it allows to set values based on a schedule and the current time. Check [specific section](#schedulervalue-details) for more details. + +### `schedulerValue` details + +Taking into account an expression like this one: + +``` +field|schedulerValue(, [ [, ], ..., [, ] ]) +``` + +where the `` are cron schedules (e.g. `* * * * SUN`) and being current date ``, the behaviour is as follows: + +* The `[, ]` pairs are evaluated from left to right +* The next "tick" is calculated based on `` and ``. For instance, if schedule is `* * * * SUN` and current time is Tuesday March 5th 2024, next tick for is Sunday March 10th 2024. If current time is Wednesday March 13th for the same schedule, next tick is Sunday March 17th. +* If `` is *before* next tick date, then we have a match and `` is returned. The i+1 to N elements in the schedulers list are not evaluated. +* `` can be a literal value (e.g. `"ok"`) or an expression (e.g. `0|currentTimeIso`) +* If none of the `[, ]` matches, then `field` is returned. + +### Interpolation transformations + +- `interpolate`: returns number interpolation, given an initial value, a final value and a number of steps. Example: `3|interpolate(0,10,9)` +- `linearInterpolator`: returns linear value interpolation, taking into account an array of values in `[number, value]` format. Example: `number|linearInterpolator([ [0,0], [1,1], [2,1.5], [8,1.8], [10,2.0]])` for number 2 returns 1.5, for number 5 returns the linear interpolation between 2 and 8, taking into account the associated values 1.5 and 1.8 respectively. +- `linearSelector`: allows to select a given value taking into account ranges defined between two elements in an array. Example: `0|rndFloat(1)|linearSelector([[0.02,'BLACK'], [0.04,'NO FLAG'], [0.06,'RED'], [0.21,'YELLOW'], [1,'GREEN']])`, if input is `0.02 < (0|rndFloat(1)) ≤ 0,04` returns `NO FLAG`. +- `randomLinearInterpolator`: returns linear value interpolation with a random factor, taking into account an array of values in `[number, value]` format. Example: `number|randomLinearInterpolator([0,1],[ [0,0], [1,1], [2,1.5], [8,1.8], [10,2.0]])` for number 2 returns a value close to 1.5 (close due to a random factor is applied), for number 5 returns the lineal interpolation between 2 and 8, taking into account the associated values 1.5 and 1.8 respectively and the random factor. The random factor is specified as a `[min, max]` array and the calculated interpolated value is multiplied by a random number between `min` and `max`. For instance, with `[0.85, 0.99]` the result will be closer to the interpolation but with `[0, 1]` the spread will be wider. +- `alertMaxValue`: returns `True` when input value is greater on equal to a given condition. Example: `0|rnd(5)|alertMaxValue(2)` returns `True` when `0|rnd(5)` is 3, 4 or 5 (for other input values result will be `False`). +- `valueResolver`: given an array `[str, value]` allows to map string with values. Example: `flag|valueResolver([['BLACK', 'stormy'], ['NO FLAG', 'curly'], ['RED', 'stormy'], ['YELLOW', 'curly'], ['GREEN', 'plain']])` is the evaluation of `flag` field is `GREEN` then the returned value would be `plain`. + +# Miscelaneous transformations + +- `typeOf`: TBD +- `strToLocation`: given a latitude and a longitude, it returns an array to build a location. Example: `"value1, value2"|strToLocation`. Example: `"value1, value2"|strToLocation` returns `[value1, value2]`, so we can use this: + +```json +{ + "init": null, + "type": "geo:json", + "exp": "{coordinates:(point|strToLocation),type: \"Point\"}" +} +``` + +## Packaging + +* Check `VERSION` value in `setup.py` file. +* Run + +```bash +python3 setup.py sdist bdist_wheel +``` + +* The file `tcjexl-.tar.gz` is generated in the `dist` directory. + +## Uploading package to pypi repository + +Once the package has been build as explained in the previous section it can be uploaded to pypi repository. + +First, install the twine tool: + +```bash +pip install twine +``` + +Next, run: + +```bash +twine upload dist/tcjexl-x.y.z.tar.gz +``` + +You need to be registered at https://pypi.org with permissions at https://pypi.org/project/tcjexl/, as during the +upload process you will be prompted to provide your user and password. ## Changelog diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ab91459 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +import pathlib +from setuptools import find_packages, setup + +HERE = pathlib.Path(__file__).parent + +VERSION = '0.0.0.post' +PACKAGE_NAME = 'tcjexl' +AUTHOR = '' +AUTHOR_EMAIL = '' +URL = '' + +LICENSE = '' #License type +DESCRIPTION = 'Wrapper of pyjexl with additional set of transformations' # Short description +LONG_DESCRIPTION = (HERE / "README.md").read_text(encoding='utf-8') # Reference to the README.md document with a longer description +LONG_DESC_TYPE = "text/markdown" + +# Required packages. They will be installed if not already installed +INSTALL_REQUIRES = [ + 'pyjexl==0.3.0', + 'croniter==1.3.15' +] + +setup( + name=PACKAGE_NAME, + version=VERSION, + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + long_description_content_type=LONG_DESC_TYPE, + author=AUTHOR, + author_email=AUTHOR_EMAIL, + url=URL, + install_requires=INSTALL_REQUIRES, + license=LICENSE, + packages=find_packages(), + include_package_data=True +) diff --git a/tcjexl/__init__.py b/tcjexl/__init__.py index 199cab2..df5023f 100644 --- a/tcjexl/__init__.py +++ b/tcjexl/__init__.py @@ -1,18 +1,18 @@ -# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. -# -# This file is part of tcjexl -# -# tcjexl is free software: you can redistribute it and/or -# modify it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# tcjexl is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -# General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. - -from tcjexl.jexl import JEXL +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +from tcjexl.jexl import JEXL diff --git a/tcjexl/functions.py b/tcjexl/functions.py new file mode 100644 index 0000000..3a0a46d --- /dev/null +++ b/tcjexl/functions.py @@ -0,0 +1,110 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +import datetime +import random +from datetime import timezone +from croniter import croniter + +def zipStrList(left: list, right: list, sep: str = ""): + l_length = len(left) + r_length = len(right) + if l_length == r_length: + return [str(left[i]) + sep + str(right[i]) for i in range(l_length)] + else: + raise ValueError(f"zipStrList input error, left length: {l_length}, right len length: {r_length}") + + +def schedulerValue(lastValue, lastRun, array): + for item in array: + cron_expression = str(item[0]) + newValue = item[1] + + if (lastRun == None): + return lastValue + + value = None + if "value" in lastRun: + value = lastRun["value"] + else: + value = lastRun + + try: + lastrun_date = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") + except: + lastrun_date = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") + current_date = datetime.datetime.now(timezone.utc) + + cron = croniter(cron_expression, lastrun_date) + next_date = cron.get_next(datetime.datetime) + next_date = next_date.astimezone(current_date.tzinfo) + + if (current_date > next_date): + return newValue + + return lastValue + + +def linearInterpolator(t, interpolations): + def interpolate(start, end, t): + return start + (end - start) * t + + for i in range(len(interpolations)): + if i < len(interpolations) - 1 and interpolations[i][0] <= t < interpolations[i + 1][0]: + start_time, start_value = interpolations[i] + end_time, end_value = interpolations[i + 1] + time_ratio = (t - start_time) / (end_time - start_time) + return interpolate(start_value, end_value, time_ratio) + + raise ValueError("Invalid input or interpolations") + + +def valueResolver(t: str, elements: list): + dictionary = {} + for k, v in elements: + dictionary[k] = v + r = dictionary.get(t, None) + if r is None: + raise ValueError(f"Invalid key {t}") + return r + + +def linearSelector(t, interpolations): + for i in range(len(interpolations)): + if t <= interpolations[i][0]: + _, start_value = interpolations[i] + return start_value + + raise ValueError("Invalid input or interpolations") + + +def randomLinearInterpolator(t, rndFactor, interpolations): + def interpolate(start, end, t): + return start + (end - start) * t + + def random_interpolate(start, end, t): + random_value = random.uniform(rndFactor[0], rndFactor[1]) + return interpolate(start, end, t) * random_value + + for i in range(len(interpolations)): + if i < len(interpolations) - 1 and interpolations[i][0] <= t < interpolations[i + 1][0]: + start_time, start_value = interpolations[i] + end_time, end_value = interpolations[i + 1] + time_ratio = (t - start_time) / (end_time - start_time) + return random_interpolate(start_value, end_value, time_ratio) + + raise ValueError("Invalid input or interpolations") diff --git a/tcjexl/jexl.py b/tcjexl/jexl.py index f4897a3..b8fd721 100644 --- a/tcjexl/jexl.py +++ b/tcjexl/jexl.py @@ -1,26 +1,78 @@ -# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. -# -# This file is part of tcjexl -# -# tcjexl is free software: you can redistribute it and/or -# modify it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# tcjexl is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -# General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. - - -import pyjexl - - -class JEXL(pyjexl.JEXL): - def __init__(self, context=None): - super().__init__(context=context) - - super().add_transform("lowercase", lambda x: str(x).lower()) +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + + +import pyjexl +import random +import datetime +import math +import functools + +from datetime import timezone +from .functions import linearInterpolator, linearSelector, randomLinearInterpolator, schedulerValue, zipStrList, valueResolver + +class JEXL(pyjexl.JEXL): + def __init__(self, context=None, now=datetime.datetime.now(timezone.utc)): + super().__init__(context=context) + + self.now = now + + # Tested by test_transforms_math.py + super().add_transform("rnd", lambda ini, end: random.randrange(ini, end)) + super().add_transform("rndFloat", lambda ini, end: random.uniform(ini, end)) + super().add_transform("round", lambda x, decimals: round(x, decimals)) + super().add_transform("floor", lambda x: math.floor(x)) + super().add_transform("parseInt", lambda x: int(x)) + super().add_transform("parseFloat", lambda x: float(x)) + + # Tested by test_transforms_string.py + super().add_transform("uppercase", lambda x: str(x).upper()) + super().add_transform("lowercase", lambda x: str(x).lower()) + super().add_transform("toString", lambda x: str(x)) + super().add_transform("substring", lambda x, ini, fin: x[ini:fin]) + super().add_transform("includes", lambda x, str: str in x) + super().add_transform("len", lambda data: len(data)) + + # Tested by test_transforms_list.py + super().add_transform("next", lambda x, arr: arr[(arr.index(x) + 1) % len(arr)]) + super().add_transform("indexOf", lambda x, str: x.index(str)) + super().add_transform("rndList", lambda init, end, length: [random.randrange(init, end) for _ in range(length)]) + super().add_transform("rndFloatList", lambda ini, end, length: [random.uniform(ini, end) for _ in range(length)]) + super().add_transform("zipStringList", zipStrList) + super().add_transform("concatList", lambda list_value: functools.reduce(lambda a, b: a + b, list_value)) + + # Tested by test_transforms_date.py + super().add_transform("currentTime", lambda x: self.now()) + super().add_transform("currentTimeIso", lambda x: self.now().strftime('%Y-%m-%dT%H:%M:%S') + ".000Z") + super().add_transform("toIsoString", lambda date: date.strftime('%Y-%m-%dT%H:%M:%S') + ".000Z") + super().add_transform("currentTimeFormat", lambda x, string: self.now().strftime(string)) + super().add_transform("timeFormat", lambda date, string: date.strftime(string)) + super().add_transform("currentHour24", lambda x: int(self.now().hour)) + super().add_transform("currentDay", lambda x: int(self.now().day)) + super().add_transform("schedulerValue", lambda lastValue, lastRun, array: schedulerValue(lastValue, lastRun, array)) + + # Tested by test_transforms_interpolation.py + super().add_transform("interpolate", lambda step, ini, end, nSteps: ((end - ini) * (step % nSteps)) / nSteps) + super().add_transform("linearInterpolator", lambda t, array: linearInterpolator(t, array)) + super().add_transform("linearSelector", lambda t, array: linearSelector(t, array)) + super().add_transform("randomLinearInterpolator", lambda t, rndFactor, array: randomLinearInterpolator(t, rndFactor, array)) + super().add_transform("alertMaxValue", lambda value, max_value: True if value >= max_value else False) + super().add_transform("valueResolver", lambda t, values: valueResolver(t, values)) + + # Tested by test_transforms_misc.py + super().add_transform("typeOf", lambda x: f'{type(x)}'[8:-2]) + super().add_transform("strToLocation", lambda str: [float(x) for x in str.split(",")]) diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..c826f74 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,30 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + + +import unittest +from tcjexl import JEXL + + +class TestBasic(unittest.TestCase): + def test_basic(self): + jexl = JEXL() + + context = {"a": 5, "b": 7} + + result = jexl.evaluate('a+b', context) + self.assertEqual(result, 12) diff --git a/tests/test_transforms_date.py b/tests/test_transforms_date.py new file mode 100644 index 0000000..4854eca --- /dev/null +++ b/tests/test_transforms_date.py @@ -0,0 +1,82 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +# This file tests the following transformations: +# +# - currentTime +# - currentTimeIso +# - toIsoString +# - currentTimeFormat +# - timeFormat +# - currentHour24 +# - currentDay +# - schedulerValue + +import unittest +import datetime +from tcjexl import JEXL + +class TestTransformsDate(unittest.TestCase): + + def setUp(self) -> None: + # Fixed time to 2024-02-01T12:43:55.123Z + def test_now(): + return datetime.datetime(2024, 2, 1, 12, 43, 55, 123, tzinfo=datetime.timezone.utc) + + self.jexl = JEXL(now=test_now) + + def test_currentTime(self): + result = self.jexl.evaluate('0|currentTime', {}) + self.assertEqual(result, datetime.datetime(2024, 2, 1, 12, 43, 55, 123, tzinfo=datetime.timezone.utc)) + + def test_currentTimeIso(self): + result = self.jexl.evaluate('0|currentTimeIso', {}) + self.assertEqual(result, '2024-02-01T12:43:55.000Z') + + def test_toIsoString(self): + result = self.jexl.evaluate('0|currentTime|toIsoString', {}) + self.assertEqual(result, '2024-02-01T12:43:55.000Z') + + def test_currentTimeFormat(self): + result = self.jexl.evaluate("0|currentTimeFormat('%Y')", {}) + self.assertEqual(result, '2024') + + def test_timeFormat(self): + result = self.jexl.evaluate("0|currentTime|timeFormat('%Y')", {}) + self.assertEqual(result, '2024') + + def test_currentHour24(self): + result = self.jexl.evaluate('0|currentHour24', {}) + self.assertEqual(result, 12) + + def test_currentDay(self): + result = self.jexl.evaluate('0|currentDay', {}) + self.assertEqual(result, 1) + + def test_schedulerValue(self): + # Current time is 2024-02-01T12:43:55.123Z, so + # next tick for "* * 2 2 *" is 2024-02-02 + context = {'a': 'no match'} + schedule = '[ ["* * 2 2 *", "match"] ]' + + # ref date before tick (<2024-02-02) + result = self.jexl.evaluate('a|schedulerValue("2024-02-01T01:00:00.00Z", ' + schedule + ')', context) + self.assertEqual(result, "match") + + # ref date after tick (>2024-02-02) + result = self.jexl.evaluate('a|schedulerValue("2024-02-05T01:00:00.00Z", ' + schedule + ')', context) + self.assertEqual(result, "no match") diff --git a/tests/test_transforms_interpolation.py b/tests/test_transforms_interpolation.py new file mode 100644 index 0000000..5cbc27f --- /dev/null +++ b/tests/test_transforms_interpolation.py @@ -0,0 +1,102 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +# This file tests the following transformations: +# +# - interpolate +# - linearInterpolator +# - linearSelector +# - randomLinearInterpolator +# - alertMaxValue +# - valueResolver + +import unittest +from tcjexl import JEXL + +class TestTransformsInterpolation(unittest.TestCase): + + def setUp(self) -> None: + self.jexl = JEXL() + + def test_interpolate(self): + result = self.jexl.evaluate('3|interpolate(0,10,9)', {}) + self.assertEqual(result, 10/3) + + def test_linearInterpolator(self): + expr = 'linearInterpolator([[0, 0], [1, 1], [2, 1.5], [8, 1.8], [10, 2.0]])' + result = self.jexl.evaluate('2|' + expr, {}) + self.assertEqual(result, 1.5) + result = self.jexl.evaluate('5|' + expr, {}) + self.assertEqual(result, 1.65) + + # Invalid usage (interpolation out of range) + self.assertRaises(ValueError, self.jexl.evaluate, '11|' + expr, {}) + + def test_linearSelector(self): + expr = 'linearSelector([[0.02, "BLACK"], [0.04, "NO FLAG"], [0.06, "RED"], [0.21, "YELLOW"], [1, "GREEN"]])' + result = self.jexl.evaluate('0.01|' + expr, {}) + self.assertEqual(result, 'BLACK') + result = self.jexl.evaluate('0.03|' + expr, {}) + self.assertEqual(result, 'NO FLAG') + result = self.jexl.evaluate('0.05|' + expr, {}) + self.assertEqual(result, 'RED') + result = self.jexl.evaluate('0.07|' + expr, {}) + self.assertEqual(result, 'YELLOW') + result = self.jexl.evaluate('0.25|' + expr, {}) + self.assertEqual(result, 'GREEN') + + # Invalid usage (interpolation out of range) + self.assertRaises(ValueError, self.jexl.evaluate, '2|' + expr, {}) + + + def test_randomLinearInterpolator(self): + expr = 'randomLinearInterpolator([0.85, 0.99], [[0, 0], [1, 1], [2, 1.5], [8, 1.8], [10, 2.0]])' + result = self.jexl.evaluate('2|' + expr, {}) + self.assertGreaterEqual(result, 1.5*0.85) + self.assertLessEqual(result, 1.5*0.99) + result = self.jexl.evaluate('5|' + expr, {}) + self.assertGreaterEqual(result, 1.65*0.85) + self.assertLessEqual(result, 1.65*0.99) + + # Invalid usage (interpolation out of range) + self.assertRaises(ValueError, self.jexl.evaluate, '11|' + expr, {}) + + def test_alertMaxValue(self): + expr = 'alertMaxValue(2)' + result = self.jexl.evaluate('2.5|' + expr, {}) + self.assertEqual(result, True) + result = self.jexl.evaluate('2|' + expr, {}) + self.assertEqual(result, True) + result = self.jexl.evaluate('1.5|' + expr, {}) + self.assertEqual(result, False) + + def test_valueResolver(self): + expr = 'valueResolver([["BLACK", "stormy"], ["NO FLAG", "curly"], ["RED", "stormy"], ["YELLOW", "curly"], ["GREEN", "plain"]])' + result = self.jexl.evaluate('"BLACK"|' + expr, {}) + self.assertEqual(result, 'stormy') + result = self.jexl.evaluate('"NO FLAG"|' + expr, {}) + self.assertEqual(result, 'curly') + result = self.jexl.evaluate('"RED"|' + expr, {}) + self.assertEqual(result, 'stormy') + result = self.jexl.evaluate('"YELLOW"|' + expr, {}) + self.assertEqual(result, 'curly') + result = self.jexl.evaluate('"GREEN"|' + expr, {}) + self.assertEqual(result, 'plain') + + # Invalid usage (unknown value) + self.assertRaises(ValueError, self.jexl.evaluate, '"UNKNOWN"|' + expr, {}) + diff --git a/tests/test_transforms_list.py b/tests/test_transforms_list.py new file mode 100644 index 0000000..f3dbdfa --- /dev/null +++ b/tests/test_transforms_list.py @@ -0,0 +1,70 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +# This file tests the following transformations: +# +# - next +# - indexOf +# - rndList +# - rndFloatList +# - zipStringList +# - concatList + + +import unittest +from tcjexl import JEXL + +class TestTransformsList(unittest.TestCase): + + def setUp(self) -> None: + self.jexl = JEXL() + + def test_next(self): + result = self.jexl.evaluate('12|next([1,2,3,12,15,18])', {}) + self.assertEqual(result, 15) + + def test_indexOf(self): + context = {'c': [1, 2, 3, 12, 15, 18]} + result = self.jexl.evaluate('c|indexOf(15)', context) + self.assertEqual(result, 4) + + def test_rndList(self): + result = self.jexl.evaluate('0|rndList(6,8)', {}) + self.assertEqual(len(result), 8) + for item in result: + self.assertGreaterEqual(item, 0) + self.assertLess(item, 6) + + def test_rndFloatList(self): + result = self.jexl.evaluate('0|rndFloatList(6,8)', {}) + self.assertEqual(len(result), 8) + for item in result: + self.assertGreaterEqual(item, 0) + self.assertLessEqual(item, 6) + + def test_zipStringList(self): + context = {'x': ['a', 'b', 'c']} + result = self.jexl.evaluate("x|zipStringList(['A', 'B', 'C'], '-')", context) + self.assertEqual(result, ['a-A', 'b-B', 'c-C']) + + # Invalid usage (list size missmatch) + self.assertRaises(ValueError, self.jexl.evaluate, "x|zipStringList(['A', 'B', 'C', 'D'], '-')", context) + + def test_concatList(self): + context = {'x': ['a', 'bbbb', 'c']} + result = self.jexl.evaluate('x|concatList', context) + self.assertEqual(result, 'abbbbc') diff --git a/tests/test_transforms_math.py b/tests/test_transforms_math.py new file mode 100644 index 0000000..b815861 --- /dev/null +++ b/tests/test_transforms_math.py @@ -0,0 +1,61 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +# This file tests the following transformations: +# +# - rnd +# - rndFloat +# - round +# - floor +# - parseInt +# - parseFloat + +import unittest +from tcjexl import JEXL + +class TestTransformsMath(unittest.TestCase): + + def setUp(self) -> None: + self.jexl = JEXL() + + def test_rnd(self): + result = self.jexl.evaluate('0|rnd(10)', {}) + self.assertGreaterEqual(result, 0) + self.assertLess(result, 10) + + def test_rndFloat(self): + result = self.jexl.evaluate('0|rndFloat(10)', {}) + self.assertGreaterEqual(result, 0) + self.assertLessEqual(result, 10) + + def test_round(self): + result = self.jexl.evaluate('4.7882|round(2)', {}) + self.assertEqual(result, 4.79) + + def test_floor(self): + result = self.jexl.evaluate('4.9|floor', {}) + self.assertEqual(result, 4) + + def test_parseInt(self): + context = {'v': '42'} + result = self.jexl.evaluate('v|parseInt', context) + self.assertEqual(result, 42) + + def test_parseFloat(self): + context = {'v': '-32.567'} + result = self.jexl.evaluate('v|parseFloat', context) + self.assertEqual(result, -32.567) diff --git a/tests/test_transforms_misc.py b/tests/test_transforms_misc.py new file mode 100644 index 0000000..d5e95df --- /dev/null +++ b/tests/test_transforms_misc.py @@ -0,0 +1,51 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +# This file tests the following transformations: +# +# - strToLocation +# - typeOf + +import unittest +from tcjexl import JEXL + +class TestTransformsMisc(unittest.TestCase): + + def setUp(self) -> None: + self.jexl = JEXL() + + + def test_typeOf(self): + context = {"a": "text", "b": 7, "c": 7.4, "d": True, "e": None, "f": {}, "g": []} + result = self.jexl.evaluate('a|typeOf', context) + self.assertEqual(result, 'str') + result = self.jexl.evaluate('b|typeOf', context) + self.assertEqual(result, 'int') + result = self.jexl.evaluate('c|typeOf', context) + self.assertEqual(result, 'float') + result = self.jexl.evaluate('d|typeOf', context) + self.assertEqual(result, 'bool') + result = self.jexl.evaluate('e|typeOf', context) + self.assertEqual(result, 'NoneType') + result = self.jexl.evaluate('f|typeOf', context) + self.assertEqual(result, 'dict') + result = self.jexl.evaluate('g|typeOf', context) + self.assertEqual(result, 'list') + + def test_strToLocation(self): + result = self.jexl.evaluate('"-10.13,24.54"|strToLocation', {}) + self.assertEqual(result, [-10.13, 24.54]) diff --git a/tests/test_transforms_string.py b/tests/test_transforms_string.py new file mode 100644 index 0000000..02f3a2e --- /dev/null +++ b/tests/test_transforms_string.py @@ -0,0 +1,69 @@ +# Copyright 2024 Telefónica Soluciones de Informática y Comunicaciones de España, S.A.U. +# +# This file is part of tcjexl +# +# tcjexl is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# tcjexl is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. + +# This file tests the following transformations: +# +# - uppercase +# - lowercase +# - toString +# - substring +# - includes +# - len + +import unittest +from tcjexl import JEXL + + +class TestTransformsString(unittest.TestCase): + + def setUp(self) -> None: + self.jexl = JEXL() + + def test_uppercase(self): + context = {'c': 'aGivenString'} + result = self.jexl.evaluate('c|uppercase', context) + self.assertEqual(result, 'AGIVENSTRING') + + def test_lowercase(self): + context = {'c': 'aGivenString'} + result = self.jexl.evaluate('c|lowercase', context) + self.assertEqual(result, 'agivenstring') + + def test_toString(self): + context = {'c': ['ZZZ', {'x': 1, 'y': 2}]} + result = self.jexl.evaluate('c|toString', context) + self.assertEqual(result, "['ZZZ', {'x': 1, 'y': 2}]") + + def test_substring(self): + context = {'c': 'aGivenString'} + result = self.jexl.evaluate('c|substring(3,6)', context) + self.assertEqual(result, 'ven') + + + def test_includes(self): + context = {'c': 'aGivenString'} + result = self.jexl.evaluate('c|includes("Given")', context) + self.assertEqual(result, True) + result = self.jexl.evaluate('c|includes("Text")', context) + self.assertEqual(result, False) + + def test_len(self): + context = {'a': 'atext', 'b': [0, 1, 2]} + result = self.jexl.evaluate('a|len', context) + self.assertEqual(result, 5) + result = self.jexl.evaluate('b|len', context) + self.assertEqual(result, 3) From 87acb63ccbd6a6441f7924efa037d49efa7dfb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 15:46:50 +0100 Subject: [PATCH 3/8] ADD gitaction to run tests --- .github/workflows/unit-tests.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/unit-tests.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..196d816 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,29 @@ +name: Python Tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + name: Run Python Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + + - name: Install dependencies + run: pip install . + + - name: Run tests + run: pytest tests/ From b0658d0e555700452afcca30d69283260b65ffb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 15:48:46 +0100 Subject: [PATCH 4/8] FIX gitaction --- .github/workflows/unit-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 196d816..2ba8bcd 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -25,5 +25,8 @@ jobs: - name: Install dependencies run: pip install . + - name: Install pytest + run: pip install pytest + - name: Run tests run: pytest tests/ From 7469614d789b539712bf294fab85811616054e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 15:54:10 +0100 Subject: [PATCH 5/8] REMOVE schedulerValue function --- README.md | 17 ----------------- setup.py | 3 +-- tcjexl/functions.py | 33 --------------------------------- tcjexl/jexl.py | 3 +-- tests/test_transforms_date.py | 13 ------------- 5 files changed, 2 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 5129adf..1325069 100644 --- a/README.md +++ b/README.md @@ -62,23 +62,6 @@ a text string - `timeFormat`: allows to format a given date with a given format. For instance, if current date is 07/07/2023 and we use `0|currentTime|timeFormat("%Y")` then `2023` will be returned. - `currentHour24`: returns current time in 24 hours format. Example: `0|currentHour24`. - `currentDay`: returns the current day of the month. Example: `0|currentDay`. -- `schedulerValue`: it allows to set values based on a schedule and the current time. Check [specific section](#schedulervalue-details) for more details. - -### `schedulerValue` details - -Taking into account an expression like this one: - -``` -field|schedulerValue(, [ [, ], ..., [, ] ]) -``` - -where the `` are cron schedules (e.g. `* * * * SUN`) and being current date ``, the behaviour is as follows: - -* The `[, ]` pairs are evaluated from left to right -* The next "tick" is calculated based on `` and ``. For instance, if schedule is `* * * * SUN` and current time is Tuesday March 5th 2024, next tick for is Sunday March 10th 2024. If current time is Wednesday March 13th for the same schedule, next tick is Sunday March 17th. -* If `` is *before* next tick date, then we have a match and `` is returned. The i+1 to N elements in the schedulers list are not evaluated. -* `` can be a literal value (e.g. `"ok"`) or an expression (e.g. `0|currentTimeIso`) -* If none of the `[, ]` matches, then `field` is returned. ### Interpolation transformations diff --git a/setup.py b/setup.py index ab91459..5210024 100644 --- a/setup.py +++ b/setup.py @@ -33,8 +33,7 @@ # Required packages. They will be installed if not already installed INSTALL_REQUIRES = [ - 'pyjexl==0.3.0', - 'croniter==1.3.15' + 'pyjexl==0.3.0' ] setup( diff --git a/tcjexl/functions.py b/tcjexl/functions.py index 3a0a46d..5c688ac 100644 --- a/tcjexl/functions.py +++ b/tcjexl/functions.py @@ -15,10 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with IoT orchestrator. If not, see http://www.gnu.org/licenses/. -import datetime import random -from datetime import timezone -from croniter import croniter def zipStrList(left: list, right: list, sep: str = ""): l_length = len(left) @@ -29,36 +26,6 @@ def zipStrList(left: list, right: list, sep: str = ""): raise ValueError(f"zipStrList input error, left length: {l_length}, right len length: {r_length}") -def schedulerValue(lastValue, lastRun, array): - for item in array: - cron_expression = str(item[0]) - newValue = item[1] - - if (lastRun == None): - return lastValue - - value = None - if "value" in lastRun: - value = lastRun["value"] - else: - value = lastRun - - try: - lastrun_date = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") - except: - lastrun_date = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") - current_date = datetime.datetime.now(timezone.utc) - - cron = croniter(cron_expression, lastrun_date) - next_date = cron.get_next(datetime.datetime) - next_date = next_date.astimezone(current_date.tzinfo) - - if (current_date > next_date): - return newValue - - return lastValue - - def linearInterpolator(t, interpolations): def interpolate(start, end, t): return start + (end - start) * t diff --git a/tcjexl/jexl.py b/tcjexl/jexl.py index b8fd721..060eee6 100644 --- a/tcjexl/jexl.py +++ b/tcjexl/jexl.py @@ -23,7 +23,7 @@ import functools from datetime import timezone -from .functions import linearInterpolator, linearSelector, randomLinearInterpolator, schedulerValue, zipStrList, valueResolver +from .functions import linearInterpolator, linearSelector, randomLinearInterpolator, zipStrList, valueResolver class JEXL(pyjexl.JEXL): def __init__(self, context=None, now=datetime.datetime.now(timezone.utc)): @@ -63,7 +63,6 @@ def __init__(self, context=None, now=datetime.datetime.now(timezone.utc)): super().add_transform("timeFormat", lambda date, string: date.strftime(string)) super().add_transform("currentHour24", lambda x: int(self.now().hour)) super().add_transform("currentDay", lambda x: int(self.now().day)) - super().add_transform("schedulerValue", lambda lastValue, lastRun, array: schedulerValue(lastValue, lastRun, array)) # Tested by test_transforms_interpolation.py super().add_transform("interpolate", lambda step, ini, end, nSteps: ((end - ini) * (step % nSteps)) / nSteps) diff --git a/tests/test_transforms_date.py b/tests/test_transforms_date.py index 4854eca..39760fa 100644 --- a/tests/test_transforms_date.py +++ b/tests/test_transforms_date.py @@ -24,7 +24,6 @@ # - timeFormat # - currentHour24 # - currentDay -# - schedulerValue import unittest import datetime @@ -67,16 +66,4 @@ def test_currentDay(self): result = self.jexl.evaluate('0|currentDay', {}) self.assertEqual(result, 1) - def test_schedulerValue(self): - # Current time is 2024-02-01T12:43:55.123Z, so - # next tick for "* * 2 2 *" is 2024-02-02 - context = {'a': 'no match'} - schedule = '[ ["* * 2 2 *", "match"] ]' - # ref date before tick (<2024-02-02) - result = self.jexl.evaluate('a|schedulerValue("2024-02-01T01:00:00.00Z", ' + schedule + ')', context) - self.assertEqual(result, "match") - - # ref date after tick (>2024-02-02) - result = self.jexl.evaluate('a|schedulerValue("2024-02-05T01:00:00.00Z", ' + schedule + ')', context) - self.assertEqual(result, "no match") From 6ea9fd2844d2f59a435396c463403d113a579912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 15:56:31 +0100 Subject: [PATCH 6/8] FIX rename functions.py to expression_functions.py --- tcjexl/{functions.py => expression_functions.py} | 0 tcjexl/jexl.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tcjexl/{functions.py => expression_functions.py} (100%) diff --git a/tcjexl/functions.py b/tcjexl/expression_functions.py similarity index 100% rename from tcjexl/functions.py rename to tcjexl/expression_functions.py diff --git a/tcjexl/jexl.py b/tcjexl/jexl.py index 060eee6..62f04ce 100644 --- a/tcjexl/jexl.py +++ b/tcjexl/jexl.py @@ -23,7 +23,7 @@ import functools from datetime import timezone -from .functions import linearInterpolator, linearSelector, randomLinearInterpolator, zipStrList, valueResolver +from .expression_functions import linearInterpolator, linearSelector, randomLinearInterpolator, zipStrList, valueResolver class JEXL(pyjexl.JEXL): def __init__(self, context=None, now=datetime.datetime.now(timezone.utc)): From 360a9cbb4d6884b5f5f168366fb162b51d7f6ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 16:18:33 +0100 Subject: [PATCH 7/8] FIX missing TBDs --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1325069..f3d87a7 100644 --- a/README.md +++ b/README.md @@ -30,24 +30,24 @@ a text string - `rnd`: returns a random number between two integers. Examples: `0|rnd(10)` returns a random number between 0 (included) and 10 (not included). `12|rnd(99)` returns a random number between 12 (included) and 99 (not included). - `rndFloat`: returns a random number between two decimal numbers. Examples: `0.2|rndFloat(12.7)` returns a random number between 0.2 and 12.7. -- `round`: rounds a number with a given number of precision digits. Example: `0.12312|round(2)` return 0.12, rounding 0.12312 to two decimals. -- `floor`: rounds a number to the lesser integer. Example: `4.9|floor` returns 4. -- `parseInt`: TBD -- `parseFloat`: TBD +- `round`: rounds a number with a given number of precision digits. Example: `0.12312|round(2)` returns `0.12`, rounding 0.12312 to two decimals. +- `floor`: rounds a number to the lesser integer. Example: `4.9|floor` returns `4`. +- `parseInt`: converts a string to integer +- `parseFloat`: coverts a string to float ### String related transformations - `uppercase`: converts a given string to uppercase. - `lowercase`: converts a given string to lowercase. -- `toString`: TBD -- `substring`: TBD -- `includes`: TBD +- `toString`: returns string representation +- `substring`: returns a substring. Examaple: `aGivenString|substring(3,6)` returns `ven`. +- `includes`: returns `True` is string passed as argument is included in the one that comes in the pipe. Example: `"aGivenString"|includes("Given")` return `True` while `"aGivenString"|includes("Text")` returns `False`. - `len`: returns the number of items in an array or the length of a string. ### List related transformations - `next`: returns the next item in an array. Example: `12|next([1,2,3,12,15,18])` returns 15. -- `indexOf`: TBD +- `indexOf`: returns the index corresponding to an item in an array. Example: `[1, 2, 3, 12, 15, 18]|indexOf(15)` returns `4`. - `rndList`: returns an array of random elements within two limits. Example: `0|rndList(6,8)` is an array with 8 items and each item is a random number between 0 and 6. - `rndFloatList`: similar to `rndList`, but with decimal numbers. - `zipStringList`: concat two arrays of the same length with a given separator. @@ -66,7 +66,7 @@ a text string ### Interpolation transformations - `interpolate`: returns number interpolation, given an initial value, a final value and a number of steps. Example: `3|interpolate(0,10,9)` -- `linearInterpolator`: returns linear value interpolation, taking into account an array of values in `[number, value]` format. Example: `number|linearInterpolator([ [0,0], [1,1], [2,1.5], [8,1.8], [10,2.0]])` for number 2 returns 1.5, for number 5 returns the linear interpolation between 2 and 8, taking into account the associated values 1.5 and 1.8 respectively. +- `linearInterpolator`: returns linear value interpolation, taking into account an array of values in `[number, value]` format. Example: `number|linearInterpolator([ [0,0], [1,1], [2,1.5], [8,1.8], [10,2.0]])` for number 2 returns `1.5`, for number 5 returns the linear interpolation between 2 and 8, taking into account the associated values 1.5 and 1.8 respectively. - `linearSelector`: allows to select a given value taking into account ranges defined between two elements in an array. Example: `0|rndFloat(1)|linearSelector([[0.02,'BLACK'], [0.04,'NO FLAG'], [0.06,'RED'], [0.21,'YELLOW'], [1,'GREEN']])`, if input is `0.02 < (0|rndFloat(1)) ≤ 0,04` returns `NO FLAG`. - `randomLinearInterpolator`: returns linear value interpolation with a random factor, taking into account an array of values in `[number, value]` format. Example: `number|randomLinearInterpolator([0,1],[ [0,0], [1,1], [2,1.5], [8,1.8], [10,2.0]])` for number 2 returns a value close to 1.5 (close due to a random factor is applied), for number 5 returns the lineal interpolation between 2 and 8, taking into account the associated values 1.5 and 1.8 respectively and the random factor. The random factor is specified as a `[min, max]` array and the calculated interpolated value is multiplied by a random number between `min` and `max`. For instance, with `[0.85, 0.99]` the result will be closer to the interpolation but with `[0, 1]` the spread will be wider. - `alertMaxValue`: returns `True` when input value is greater on equal to a given condition. Example: `0|rnd(5)|alertMaxValue(2)` returns `True` when `0|rnd(5)` is 3, 4 or 5 (for other input values result will be `False`). @@ -74,7 +74,7 @@ a text string # Miscelaneous transformations -- `typeOf`: TBD +- `typeOf`: returns type representation of the data (e.g. `str`, `int`, `float`, etc.) - `strToLocation`: given a latitude and a longitude, it returns an array to build a location. Example: `"value1, value2"|strToLocation`. Example: `"value1, value2"|strToLocation` returns `[value1, value2]`, so we can use this: ```json From 2cd1a3fc07fa9649b0face0b8fcf1d63ea09f06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Gal=C3=A1n=20M=C3=A1rquez?= Date: Tue, 5 Mar 2024 16:25:13 +0100 Subject: [PATCH 8/8] FIX remove blank lines --- tests/test_transforms_date.py | 2 -- tests/test_transforms_interpolation.py | 1 - tests/test_transforms_misc.py | 1 - tests/test_transforms_string.py | 1 - 4 files changed, 5 deletions(-) diff --git a/tests/test_transforms_date.py b/tests/test_transforms_date.py index 39760fa..007df01 100644 --- a/tests/test_transforms_date.py +++ b/tests/test_transforms_date.py @@ -65,5 +65,3 @@ def test_currentHour24(self): def test_currentDay(self): result = self.jexl.evaluate('0|currentDay', {}) self.assertEqual(result, 1) - - diff --git a/tests/test_transforms_interpolation.py b/tests/test_transforms_interpolation.py index 5cbc27f..5fd638a 100644 --- a/tests/test_transforms_interpolation.py +++ b/tests/test_transforms_interpolation.py @@ -99,4 +99,3 @@ def test_valueResolver(self): # Invalid usage (unknown value) self.assertRaises(ValueError, self.jexl.evaluate, '"UNKNOWN"|' + expr, {}) - diff --git a/tests/test_transforms_misc.py b/tests/test_transforms_misc.py index d5e95df..43d4f53 100644 --- a/tests/test_transforms_misc.py +++ b/tests/test_transforms_misc.py @@ -28,7 +28,6 @@ class TestTransformsMisc(unittest.TestCase): def setUp(self) -> None: self.jexl = JEXL() - def test_typeOf(self): context = {"a": "text", "b": 7, "c": 7.4, "d": True, "e": None, "f": {}, "g": []} result = self.jexl.evaluate('a|typeOf', context) diff --git a/tests/test_transforms_string.py b/tests/test_transforms_string.py index 02f3a2e..d24fd98 100644 --- a/tests/test_transforms_string.py +++ b/tests/test_transforms_string.py @@ -53,7 +53,6 @@ def test_substring(self): result = self.jexl.evaluate('c|substring(3,6)', context) self.assertEqual(result, 'ven') - def test_includes(self): context = {'c': 'aGivenString'} result = self.jexl.evaluate('c|includes("Given")', context)