From 90876c97fb4e8b88916aa0a47cdfb2f885d73aa7 Mon Sep 17 00:00:00 2001 From: Pierre Clisson Date: Sat, 13 Jul 2024 17:21:07 +0200 Subject: [PATCH] Initial commit --- .github/workflows/build.yml | 41 ++++++++++++++ .github/workflows/publish.yml | 28 ++++++++++ .gitignore | 13 +++++ .readthedocs.yaml | 14 +++++ LICENSE | 21 ++++++++ README.rst | 17 ++++++ examples/dynamic.yaml | 27 ++++++++++ examples/dynamic_prefixed.yaml | 30 +++++++++++ examples/multi.yaml | 50 +++++++++++++++++ examples/sine.yaml | 22 ++++++++ examples/sinus.yaml | 19 +++++++ examples/test.yaml | 27 ++++++++++ pyproject.toml | 11 ++++ setup.cfg | 42 +++++++++++++++ setup.py | 3 ++ test/conftest.py | 7 +++ test/test_arithmetic.py | 25 +++++++++ timeflux_example/__init__.py | 0 timeflux_example/nodes/__init__.py | 0 timeflux_example/nodes/arithmetic.py | 80 ++++++++++++++++++++++++++++ timeflux_example/nodes/dynamic.py | 67 +++++++++++++++++++++++ timeflux_example/nodes/signal.py | 46 ++++++++++++++++ timeflux_example/nodes/sinus.py | 44 +++++++++++++++ 23 files changed, 634 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 .readthedocs.yaml create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 examples/dynamic.yaml create mode 100644 examples/dynamic_prefixed.yaml create mode 100644 examples/multi.yaml create mode 100644 examples/sine.yaml create mode 100644 examples/sinus.yaml create mode 100644 examples/test.yaml create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test/conftest.py create mode 100644 test/test_arithmetic.py create mode 100644 timeflux_example/__init__.py create mode 100644 timeflux_example/nodes/__init__.py create mode 100644 timeflux_example/nodes/arithmetic.py create mode 100644 timeflux_example/nodes/dynamic.py create mode 100644 timeflux_example/nodes/signal.py create mode 100644 timeflux_example/nodes/sinus.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dd744aa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Set env + run: echo "PACKAGE=$(basename `git config --get remote.origin.url` | sed -e 's/\.git$//')" >> $GITHUB_ENV + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: Test formatting + run: | + pip install black + black --check $PACKAGE + - name: Test documentation + run: | + cd doc + make html + cd .. + - name: Test code + run: | + pip install pytest pytest-cov + pytest --cov=$PACKAGE diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..80c23a7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,28 @@ +name: publish + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine pep517 + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python -m pep517.build . + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb55e41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +.cache +.pytest_cache +__pycache__ +*.py[cod] +*$py.class +*.egg-info +build/ +dist/ +.idea +.coverage + +doc/** \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..fd1def8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 +formats: + - pdf +python: + version: 3.8 + install: + - method: pip + path: . + extra_requirements: + - dev + - method: setuptools + path: . +sphinx: + fail_on_warning: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02cca62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018, Pierre Clisson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..5e8667b --- /dev/null +++ b/README.rst @@ -0,0 +1,17 @@ +An example Timeflux plugin +========================== + +This is an example plugin that provides a few simple demonstration nodes. Use it as a template +to develop your own plugins. + +Installation +------------ + +First, make sure that `Timeflux `__ is installed. + +You can then install this plugin in the `timeflux` environment: + +:: + + $ conda activate timeflux + $ pip install timeflux_example diff --git a/examples/dynamic.yaml b/examples/dynamic.yaml new file mode 100644 index 0000000..bc67834 --- /dev/null +++ b/examples/dynamic.yaml @@ -0,0 +1,27 @@ +graphs: + + - id: DynamicIO + + nodes: + - id: node_1 + module: timeflux_example.nodes.dynamic + class: Outputs + params: + seed: 1 + - id: node_2 + module: timeflux_example.nodes.dynamic + class: Inputs + - id: node_3 + module: timeflux.nodes.debug + class: Display + - id: node_4 + module: timeflux.nodes.debug + class: Display + + edges: + - source: node_1:* # The magic happens here + target: node_2 + - source: node_1 + target: node_3 + - source: node_2 + target: node_4 \ No newline at end of file diff --git a/examples/dynamic_prefixed.yaml b/examples/dynamic_prefixed.yaml new file mode 100644 index 0000000..c301ce3 --- /dev/null +++ b/examples/dynamic_prefixed.yaml @@ -0,0 +1,30 @@ +graphs: + + - id: DynamicIO + + nodes: + - id: node_1 + module: timeflux_example.nodes.dynamic + class: Outputs + params: + seed: 1 + prefix: foo + - id: node_2 + module: timeflux_example.nodes.dynamic + class: Inputs + params: + prefix: bar + - id: node_3 + module: timeflux.nodes.debug + class: Display + - id: node_4 + module: timeflux.nodes.debug + class: Display + + edges: + - source: node_1:foo_* # Dynamic inputs can be prefixed + target: node_2:bar # The same goes for outputs + - source: node_1 + target: node_3 + - source: node_2 + target: node_4 \ No newline at end of file diff --git a/examples/multi.yaml b/examples/multi.yaml new file mode 100644 index 0000000..4305ba3 --- /dev/null +++ b/examples/multi.yaml @@ -0,0 +1,50 @@ +graphs: + + - id: multi + nodes: + - id: matrix_1 + module: timeflux.nodes.random + class: Random + params: + columns: 2 + rows_min: 2 + rows_max: 2 + value_min: 1 + value_max: 1 + seed: 1 + - id: matrix_2 + module: timeflux.nodes.random + class: Random + params: + columns: 2 + rows_min: 2 + rows_max: 2 + value_min: 2 + value_max: 2 + seed: 1 + - id: matrix_add + module: timeflux_example.nodes.arithmetic + class: MatrixAdd + - id: display_matrix_1 + module: timeflux.nodes.debug + class: Display + - id: display_matrix_2 + module: timeflux.nodes.debug + class: Display + - id: display_matrix_add + module: timeflux.nodes.debug + class: Display + + edges: + - source: matrix_1 + target: matrix_add:m1 + - source: matrix_2 + target: matrix_add:m2 + - source: matrix_1 + target: display_matrix_1 + - source: matrix_2 + target: display_matrix_2 + - source: matrix_add + target: display_matrix_add + + rate: 0.1 \ No newline at end of file diff --git a/examples/sine.yaml b/examples/sine.yaml new file mode 100644 index 0000000..4873c93 --- /dev/null +++ b/examples/sine.yaml @@ -0,0 +1,22 @@ +graphs: + - nodes: + - id: sine + module: timeflux_example.nodes.signal + class: Sine + params: + frequency: 120 + amplitude: 1 + resolution: 44100 + - id: ui + module: timeflux_ui.nodes.ui + class: UI + params: + settings: + monitor: + millisPerPixel: 0.25 + lineWidth: 1 + interpolation: linear + edges: + - source: sine + target: ui:sine + rate: 10 \ No newline at end of file diff --git a/examples/sinus.yaml b/examples/sinus.yaml new file mode 100644 index 0000000..344bd0c --- /dev/null +++ b/examples/sinus.yaml @@ -0,0 +1,19 @@ +graphs: + + - nodes: + - id: sinus + module: timeflux_example.nodes.sinus + class: Sinus + params: + rate: 1 + amplitude: 1 + + - id: ui + module: timeflux_ui.nodes.ui + class: UI + + edges: + - source: sinus + target: ui:sinus + + rate: 100 \ No newline at end of file diff --git a/examples/test.yaml b/examples/test.yaml new file mode 100644 index 0000000..696612d --- /dev/null +++ b/examples/test.yaml @@ -0,0 +1,27 @@ +graphs: + + - nodes: + - id: node_1 + module: timeflux.nodes.random + class: Random + params: + columns: 5 + rows_min: 1 + rows_max: 10 + value_min: 0 + value_max: 5 + seed: 1 + - id: node_2 + module: timeflux_example.nodes.arithmetic + class: Add + params: + value: 1 + - id: node_3 + module: timeflux.nodes.debug + class: Display + + edges: + - source: node_1 + target: node_2 + - source: node_2 + target: node_3 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c8e3239 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +# Enable version inference +[tool.setuptools_scm] +local_scheme = "no-local-version" + +# Generate documentation shim +[tool.docinit] + +# https://setuptools.readthedocs.io/en/latest/build_meta.html +[build-system] +requires = ["setuptools>=62", "wheel", "setuptools_scm[toml]>=3.4", "docinit"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..82bc236 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,42 @@ +[metadata] +name = timeflux-example +description = An example Timeflux plugin +long_description = file: README.rst +author = Pierre Clisson +author-email = contact@timeflux.io +license = MIT +home-page = https://timeflux.io +project_urls = + Documentation = http://doc.timeflux.io/projects/timeflux-example/ + Source Code = https://github.com/timeflux/timeflux_example + Bug Tracker = https://github.com/timeflux/timeflux_example/issues +classifier = + Development Status :: 4 - Beta + Environment :: Console + Intended Audience :: Developers + Intended Audience :: Science/Research + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python +keywords = timeflux + +[options] +packages = find: +install_requires = + timeflux>=0.5.3 + +[options.extras_require] +dev = + pytest>=5.3 + sphinx>=2.2 + sphinx_rtd_theme>=0.4 + setuptools_scm + docinit + +[docinit] +name = Example plugin +parent_url = https://doc.timeflux.io +logo_url = https://github.com/timeflux/timeflux/raw/master/doc/static/img/logo.png + +[build_sphinx] +warning-is-error = 1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..871c2d0 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +# Required for editable installs +# https://discuss.python.org/t/specification-of-editable-installation/1564/ +import setuptools; setuptools.setup() diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..d88068f --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,7 @@ +"""Test configuration""" + +import os +import pytest + +def pytest_configure(config): + pytest.path = os.path.dirname(os.path.abspath(__file__)) diff --git a/test/test_arithmetic.py b/test/test_arithmetic.py new file mode 100644 index 0000000..ff6041d --- /dev/null +++ b/test/test_arithmetic.py @@ -0,0 +1,25 @@ +"""Tests for arithmetic.py""" + +import pytest +import pandas as pd +from pandas.testing import assert_frame_equal +from timeflux.core.io import Port +from timeflux_example.nodes.arithmetic import Add, MatrixAdd + +def test_add(): + node = Add(1) + node.i = Port() + node.i.data = pd.DataFrame([[1, 1], [1, 1]]) + node.update() + expected = pd.DataFrame([[2, 2], [2, 2]]) + assert_frame_equal(node.o.data, expected) + +def test_matrix(): + node = MatrixAdd() + node.i_m1 = Port() + node.i_m2 = Port() + node.i_m1.data = pd.DataFrame([[1, 1], [1, 1]]) + node.i_m2.data = pd.DataFrame([[2, 2], [2, 2]]) + node.update() + expected = pd.DataFrame([[3, 3], [3, 3]]) + assert_frame_equal(node.o.data, expected) diff --git a/timeflux_example/__init__.py b/timeflux_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/timeflux_example/nodes/__init__.py b/timeflux_example/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/timeflux_example/nodes/arithmetic.py b/timeflux_example/nodes/arithmetic.py new file mode 100644 index 0000000..2351884 --- /dev/null +++ b/timeflux_example/nodes/arithmetic.py @@ -0,0 +1,80 @@ +"""Simple example nodes""" + +from timeflux.core.node import Node + + +class Add(Node): + + """Adds ``value`` to each cell of the input. + + This is one of the simplest possible nodes. + + Attributes: + i (Port): Default input, expects DataFrame. + o (Port): Default output, provides DataFrame. + + Example: + .. literalinclude:: /../examples/test.yaml + :language: yaml + """ + + def __init__(self, value): + """ + Args: + value (int): The value to add to each cell. + """ + self._value = value + + def update(self): + # Make sure we have a non-empty dataframe + if self.i.ready(): + # Copy the input to the output + self.o = self.i + # Add the value to each cell + self.o.data += self._value + + +class MatrixAdd(Node): + + """Sum two input matrices together. + + This node illustrates multiple named inputs. + Note that it is not necessary to declare the ports. They will be created dynamically. + + Attributes: + i_m1 (Port): First matrix, expects DataFrame. + i_m2 (Port): Second matrix, expects DataFrame. + o (Port): Default output, provides DataFrame. + + Example: + .. literalinclude:: /../examples/multi.yaml + :language: yaml + """ + + def __init__(self): + pass + + def update(self): + # propagate the meta + self.o.meta = self.i_m1.meta + self.o.meta.update(self.i_m2.meta) + # sum the data + self.o.data = self.i_m1.data + self.i_m2.data + + +class MatrixDivide(Node): + + """Divide one matrix by another. + + Attributes: + i_m1 (Port): First matrix, expects DataFrame. + i_m2 (Port): Second matrix, expects DataFrame. + o (Port): Default output, provides DataFrame. + + """ + + def __init__(self): + pass + + def update(self): + self.o.data = self.i_m1.data.divide(self.i_m2.data) diff --git a/timeflux_example/nodes/dynamic.py b/timeflux_example/nodes/dynamic.py new file mode 100644 index 0000000..6148d2e --- /dev/null +++ b/timeflux_example/nodes/dynamic.py @@ -0,0 +1,67 @@ +"""Illustrates dynamic inputs and outputs.""" + +import random +from timeflux.core.node import Node + + +class Outputs(Node): + + """Randomly generate dynamic outputs. + + At each update, this node generates a random number of outputs and sets the default output + to the number it has created. + + Attributes: + o (Port): Default output, provides DataFrame. + o_* (Port): Dynamic outputs. + + Args: + seed (int): The random number generator seed. + prefix (string): The prefix to add to each dynamic output. + + Example: + .. literalinclude:: /../examples/dynamic_prefixed.yaml + :language: yaml + """ + + def __init__(self, prefix=None, seed=None): + random.seed(seed) + self.prefix = "" if prefix is None else prefix + "_" + + def update(self): + # Lazily create new ports + for i in range(random.randint(0, 10)): + getattr(self, "o_" + self.prefix + str(i)) + # Count + outputs = len(list(self.iterate("o_*"))) + # Set default output + self.o.set([[outputs]], names=["outputs"]) + + +class Inputs(Node): + + """Count the dynamic outputs. + + At each update, this node loops over all dynamic inputs and sets the default output + to the number it has found. + + Attributes: + i_* (Port): Dynamic inputs. + o (Port): Default output, provides DataFrame. + + Args: + prefix (string): The prefix to add to match dynamic inputs. + + Example: + .. literalinclude:: /../examples/dynamic.yaml + :language: yaml + """ + + def __init__(self, prefix=None): + self.prefix = "" if prefix is None else prefix + "_" + + def update(self): + # Count + inputs = len(list(self.iterate("i_" + self.prefix + "*"))) + # Set default output + self.o.set([[inputs]], names=["inputs"]) diff --git a/timeflux_example/nodes/signal.py b/timeflux_example/nodes/signal.py new file mode 100644 index 0000000..5095f1f --- /dev/null +++ b/timeflux_example/nodes/signal.py @@ -0,0 +1,46 @@ +"""timeflux_example.nodes.signal: generate signals""" + +import time +import numpy as np +from timeflux.core.node import Node + + +class Sine(Node): + """Generate a sinusoidal signal. + + Attributes: + o (Port): Default output, provides DataFrame. + + Args: + frequency (float): cycles per second. Default: ``1``. + resolution (int): points per second. Default: ``200``. + amplitude (float): signal amplitude. Default: ``1``. + name (string): signal name. Default: ``sine``. + + Example: + .. literalinclude:: /../examples/sine.yaml + :language: yaml + """ + + def __init__(self, frequency=1, resolution=200, amplitude=1, name="sine"): + self._frequency = frequency + self._resolution = int(resolution) + self._amplitude = amplitude + self._name = name + self._radian = 0 + self._now = time.time() + + def update(self): + now = time.time() + elapsed = now - self._now + points = int(elapsed * self._resolution) + 1 + cycles = self._frequency * elapsed + values = np.linspace(self._radian, np.pi * 2 * cycles + self._radian, points) + signal = np.sin(values[:-1]) * self._amplitude + timestamps = np.linspace( + int(self._now * 1e6), int(now * 1e6), points, False, dtype="datetime64[us]" + )[1:] + self._now = now + self._radian = values[-1] + self.o.set(signal, timestamps, names=[self._name]) + self.o.meta = {"rate": self._frequency} diff --git a/timeflux_example/nodes/sinus.py b/timeflux_example/nodes/sinus.py new file mode 100644 index 0000000..66bde20 --- /dev/null +++ b/timeflux_example/nodes/sinus.py @@ -0,0 +1,44 @@ +"""timeflux_example.nodes.sinus: generate sinusoidal signal""" + +import numpy as np +from timeflux.core.node import Node +from timeflux.helpers.clock import time_to_float, float_to_time, now +from timeflux.core.registry import Registry + + +class Sinus(Node): + """Return a sinusoidal signal sampled to registry rate. + + This node generates a sinusoidal signal of chosen frequency and amplitude. + Note that at each update, the node generate one row, so its sampling rate + equals the graph parsing rate (given by the Registry). + + Attributes: + o (Port): Default output, provides DataFrame. + + Example: + .. literalinclude:: /../examples/sinus.yaml + :language: yaml + + .. deprecated:: + Use :func:`timeflux_example.nodes.signal.Sine` instead. + + """ + + def __init__(self, amplitude=1, rate=1, name="sinus"): + self._amplitude = amplitude + self._rate = rate + self._name = name + self._start = None + + def update(self): + timestamp = now() + float = time_to_float(timestamp) + if self._start is None: + self._start = float + + values = [ + self._amplitude * np.sin(2 * np.pi * self._rate * (float - self._start)) + ] + self.o.set(values, names=[self._name]) + self.o.meta = {"rate": Registry.rate}