diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d7f3b1c4..9068cef8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,9 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: compas-dev/compas-actions.docs@v4 + - uses: compas-dev/compas-actions.docs@v5 with: github_token: ${{ secrets.GITHUB_TOKEN }} use_conda: false + generator: mkdocs + extras: dev,mkdocs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7142634b..8752f49c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* Migrated documentation to mkdocs. + ### Removed diff --git a/LICENSE b/LICENSE index 2cd68cc6..a3443640 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Gramazio Kohler Research +Copyright (c) 2026 COMPAS Association Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/_images/background_task.png b/docs/_images/background_task.png new file mode 100644 index 00000000..3603fbb4 Binary files /dev/null and b/docs/_images/background_task.png differ diff --git a/docs/_images/message.png b/docs/_images/message.png new file mode 100644 index 00000000..af649018 Binary files /dev/null and b/docs/_images/message.png differ diff --git a/docs/_images/mqtt_connect.png b/docs/_images/mqtt_connect.png new file mode 100644 index 00000000..b5877cd8 Binary files /dev/null and b/docs/_images/mqtt_connect.png differ diff --git a/docs/_images/publish.png b/docs/_images/publish.png new file mode 100644 index 00000000..c0e24f70 Binary files /dev/null and b/docs/_images/publish.png differ diff --git a/docs/_images/subscribe.png b/docs/_images/subscribe.png new file mode 100644 index 00000000..d1a0efe6 Binary files /dev/null and b/docs/_images/subscribe.png differ diff --git a/docs/_images/zenoh_connect.png b/docs/_images/zenoh_connect.png new file mode 100644 index 00000000..22a33d9c Binary files /dev/null and b/docs/_images/zenoh_connect.png differ diff --git a/docs/_logo/compas_logo_white_transparent.png b/docs/_logo/compas_logo_white_transparent.png new file mode 100644 index 00000000..5dd158c8 Binary files /dev/null and b/docs/_logo/compas_logo_white_transparent.png differ diff --git a/docs/_logo/favicon.ico b/docs/_logo/favicon.ico new file mode 100644 index 00000000..381bd941 Binary files /dev/null and b/docs/_logo/favicon.ico differ diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index d80be948..00000000 --- a/docs/api.rst +++ /dev/null @@ -1,14 +0,0 @@ -******************************************************************************** -API Reference -******************************************************************************** - -.. toctree:: - :maxdepth: 1 - - api/compas_eve - api/compas_eve.codecs - api/compas_eve.memory - api/compas_eve.mqtt - api/compas_eve.zenoh - api/compas_eve.ghpython - diff --git a/docs/api/compas_eve.codecs.md b/docs/api/compas_eve.codecs.md new file mode 100644 index 00000000..b34ca990 --- /dev/null +++ b/docs/api/compas_eve.codecs.md @@ -0,0 +1 @@ +# ::: compas_eve.codecs \ No newline at end of file diff --git a/docs/api/compas_eve.codecs.rst b/docs/api/compas_eve.codecs.rst deleted file mode 100644 index 9ac83321..00000000 --- a/docs/api/compas_eve.codecs.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_eve.codecs diff --git a/docs/api/compas_eve.ghpython.md b/docs/api/compas_eve.ghpython.md new file mode 100644 index 00000000..32c3cf48 --- /dev/null +++ b/docs/api/compas_eve.ghpython.md @@ -0,0 +1 @@ +# ::: compas_eve.ghpython \ No newline at end of file diff --git a/docs/api/compas_eve.ghpython.rst b/docs/api/compas_eve.ghpython.rst deleted file mode 100644 index 98122d77..00000000 --- a/docs/api/compas_eve.ghpython.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_eve.ghpython diff --git a/docs/api/compas_eve.md b/docs/api/compas_eve.md new file mode 100644 index 00000000..f04e7675 --- /dev/null +++ b/docs/api/compas_eve.md @@ -0,0 +1 @@ +# ::: compas_eve \ No newline at end of file diff --git a/docs/api/compas_eve.memory.md b/docs/api/compas_eve.memory.md new file mode 100644 index 00000000..265208a3 --- /dev/null +++ b/docs/api/compas_eve.memory.md @@ -0,0 +1 @@ +# ::: compas_eve.memory \ No newline at end of file diff --git a/docs/api/compas_eve.memory.rst b/docs/api/compas_eve.memory.rst deleted file mode 100644 index 68205230..00000000 --- a/docs/api/compas_eve.memory.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_eve.memory diff --git a/docs/api/compas_eve.mqtt.md b/docs/api/compas_eve.mqtt.md new file mode 100644 index 00000000..68832915 --- /dev/null +++ b/docs/api/compas_eve.mqtt.md @@ -0,0 +1 @@ +# ::: compas_eve.mqtt \ No newline at end of file diff --git a/docs/api/compas_eve.mqtt.rst b/docs/api/compas_eve.mqtt.rst deleted file mode 100644 index d8a3532e..00000000 --- a/docs/api/compas_eve.mqtt.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_eve.mqtt diff --git a/docs/api/compas_eve.rst b/docs/api/compas_eve.rst deleted file mode 100644 index 625f24fe..00000000 --- a/docs/api/compas_eve.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: compas_eve diff --git a/docs/api/compas_eve.zenoh.md b/docs/api/compas_eve.zenoh.md new file mode 100644 index 00000000..f4525f32 --- /dev/null +++ b/docs/api/compas_eve.zenoh.md @@ -0,0 +1 @@ +# ::: compas_eve.zenoh \ No newline at end of file diff --git a/docs/api/compas_eve.zenoh.rst b/docs/api/compas_eve.zenoh.rst deleted file mode 100644 index 038b76d4..00000000 --- a/docs/api/compas_eve.zenoh.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_eve.zenoh diff --git a/docs/assets/stylesheets/custom.css b/docs/assets/stylesheets/custom.css new file mode 100644 index 00000000..d341f692 --- /dev/null +++ b/docs/assets/stylesheets/custom.css @@ -0,0 +1,5 @@ +:root>* { + --md-primary-fg-color: #0092d2; + --md-primary-fg-color--light: #0092d2; + --md-primary-fg-color--dark: #0092d2; +} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index f07772d8..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,166 +0,0 @@ -# flake8: noqa -# -*- coding: utf-8 -*- - -from sphinx.writers import html, html5 -import sphinx_compas2_theme - -# -- General configuration ------------------------------------------------ - -project = "COMPAS EVE" -copyright = "Gramazio Kohler Research" -author = "Gonzalo Casas" -organization = "compas-dev" -package = "compas_eve" - -master_doc = "index" -source_suffix = {".rst": "restructuredtext", ".md": "markdown"} -templates_path = sphinx_compas2_theme.get_autosummary_templates_path() -exclude_patterns = sphinx_compas2_theme.default_exclude_patterns -add_module_names = True -language = "en" - -latest_version = sphinx_compas2_theme.get_latest_version() - -if latest_version == "Unreleased": - release = "Unreleased" - version = "latest" -else: - release = latest_version - version = ".".join(release.split(".")[0:2]) # type: ignore - -# -- Extension configuration ------------------------------------------------ - -extensions = sphinx_compas2_theme.default_extensions - -# numpydoc options - -numpydoc_show_class_members = False -numpydoc_class_members_toctree = False -numpydoc_attributes_as_param_list = True -numpydoc_show_inherited_class_members = False - -# bibtex options - -# autodoc options - -autodoc_type_aliases = {} -autodoc_typehints_description_target = "documented" -autodoc_mock_imports = sphinx_compas2_theme.default_mock_imports -autodoc_default_options = { - "undoc-members": True, - "show-inheritance": True, -} -autodoc_member_order = "groupwise" -autodoc_typehints = "description" -autodoc_class_signature = "separated" - -autoclass_content = "class" - - -def setup(app): - app.connect("autodoc-skip-member", sphinx_compas2_theme.skip) - - -# autosummary options - -autosummary_generate = True -autosummary_mock_imports = sphinx_compas2_theme.default_mock_imports - -# graph options - -# plot options - -plot_include_source = False -plot_html_show_source_link = False -plot_html_show_formats = False -plot_formats = ["png"] - -# intersphinx options - -intersphinx_mapping = { - "python": ("https://docs.python.org/", None), - "compas": ("https://compas.dev/compas/latest/", None), -} - -# linkcode - -linkcode_resolve = sphinx_compas2_theme.get_linkcode_resolve(organization, package) - -# extlinks - -extlinks = { - "rhino": ("https://developer.rhino3d.com/api/RhinoCommon/html/T_%s.htm", "%s"), - "blender": ("https://docs.blender.org/api/2.93/%s.html", "%s"), -} - -# from pytorch - -sphinx_compas2_theme.replace(html.HTMLTranslator) -sphinx_compas2_theme.replace(html5.HTML5Translator) - -# -- Options for HTML output ---------------------------------------------- - -html_theme = "sidebaronly" -html_title = project -html_sidebars = {"index": []} - -favicons = [ - { - "rel": "icon", - "href": "compas.ico", - } -] - -html_theme_options = { - "external_links": [ - {"name": "COMPAS Framework", "url": "https://compas.dev"}, - ], - "icon_links": [ - { - "name": "GitHub", - "url": f"https://github.com/{organization}/{package}", - "icon": "fa-brands fa-github", - "type": "fontawesome", - }, - { - "name": "Discourse", - "url": "http://forum.compas-framework.org/", - "icon": "fa-brands fa-discourse", - "type": "fontawesome", - }, - { - "name": "PyPI", - "url": f"https://pypi.org/project/{package}/", - "icon": "fa-brands fa-python", - "type": "fontawesome", - }, - ], - "switcher": { - "json_url": f"https://raw.githubusercontent.com/{organization}/{package}/gh-pages/versions.json", - "version_match": version, - }, - "logo": { - "image_light": "_static/compas_icon_white.png", - "image_dark": "_static/compas_icon_white.png", - "text": "COMPAS docs", - }, - "navigation_depth": 2, -} - -html_context = { - "github_url": "https://github.com", - "github_user": organization, - "github_repo": package, - "github_version": "main", - "doc_path": "docs", -} - -html_static_path = sphinx_compas2_theme.get_html_static_path() + ["_static"] -html_css_files = [] -html_extra_path = [] -html_last_updated_fmt = "" -html_copy_source = False -html_show_sourcelink = True -html_permalinks = False -html_permalinks_icon = "" -html_compact_lists = True diff --git a/docs/examples.rst b/docs/examples.md similarity index 56% rename from docs/examples.rst rename to docs/examples.md index 1707fb9a..461f913f 100644 --- a/docs/examples.rst +++ b/docs/examples.md @@ -1,21 +1,16 @@ -******************************************************************************** -Examples -******************************************************************************** +# Examples -.. currentmodule:: compas_eve +!!! note + This tutorial assumes that you have already installed `compas_eve`. + If you haven't, please follow the instructions in the [installation](installation.md) section. -.. note:: - - This tutorial assumes that you have already installed ``compas_eve``. - If you haven't, please follow the instructions in the :ref:`installation` section. - -The main feature of ``compas_eve`` is to allow communication between different +The main feature of `compas_eve` is to allow communication between different parts of a program using messages. These messages are sent around using a publisher/subscriber model, or pub/sub for short. In pub/sub communication, messages are not sent directly from a sender to a receiver, instead, they are -sent to a :class:`Topic`. A topic is like a mailbox, the :class:`Publisher` +sent to a [Topic][compas_eve.Topic]. A topic is like a mailbox, the [Publisher][compas_eve.Publisher] sends messages to the topic without the need for a subcriber to be -actively listening for messages, and also the :class:`Subscriber` can start +actively listening for messages, and also the [Subscriber][compas_eve.Subscriber] can start listening for messages on a topic without the need for any publisher to be currently sending anything. @@ -26,43 +21,42 @@ An additional benefit of pub/sub is that it is not limited to 1-to-1 communicati on a single topic, there can be multiple publishers and multiple subscribers all communicating at the same time without additional coordination. -Hello World ------------ +## Hello World -Let's see a **Hello World** example of this type of communication using ``compas_eve``. +Let's see a **Hello World** example of this type of communication using `compas_eve`. This example is very contrived because both the publisher and the subscriber are defined in the same script and the same thread. -.. literalinclude :: examples/01_hello_world.py - :language: python +```python +--8<-- "docs/examples/01_hello_world.py" +``` This example is the simplest possible, and it only shows the main concepts needed -to communicate. In particular, ``compas_eve`` uses by default an **in-memory transport** +to communicate. In particular, `compas_eve` uses by default an **in-memory transport** for the messages, this means that messages are can only be received within the same program. -Hello Threaded World --------------------- +## Hello Threaded World Let's try to extend this first example and add multiple threads to illustrate multi-threaded communication: -.. literalinclude :: examples/02_hello_threaded_world.py - :language: python +```python +--8<-- "docs/examples/02_hello_threaded_world.py" +``` + +This get more interesting! Now the publisher and subscriber are in separate threads. +However, the in-memory transport is limited to *same-process*. This means that if we launch this script twice, the messages will not jump from one process to the other. -This get more interesting! Now the publisher and subscriber are in separate threads. However, -the in-memory transport is limited to *same-process*. This means that if we launch this -script twice, the messages will not jump from one process to the other. -In other words, if we want to communicate with a subscriber on a different process on the machine, -or even on a completely separate machine, we need to take an extra step. +In other words, if we want to communicate with a subscriber on a different process on +the machine, or even on a completely separate machine, we need to take an extra step. -Hello Distributed World ------------------------ +## Hello Distributed World Fortunately, it is very easy to extend our example and enable communication across processes, machines, networks, continents, and anything that is connected to the Internet! -The only difference is that we are going to configure a different :class:`Transport` implementation for -our messages. In this case, we will use the MQTT transport method. `MQTT `_ +The only difference is that we are going to configure a different [Transport][compas_eve.Transport] implementation for +our messages. In this case, we will use the MQTT transport method. [MQTT](https://en.wikipedia.org/wiki/MQTT) is a network protocol very popular for IoT applications because of its lightweight. We are going to split the code and create one script for sending messages with a publisher and a different @@ -71,13 +65,17 @@ potentially from different machines! First the publisher example: -.. literalinclude :: examples/03_hello_distributed_world_pub.py - :language: python +```python +--8<-- "docs/examples/03_hello_distributed_world_pub.py" +``` + +### Subscriber And now the subscriber example: -.. literalinclude :: examples/03_hello_distributed_world_sub.py - :language: python +```python +--8<-- "docs/examples/03_hello_distributed_world_sub.py" +``` You can start both programs in two completely different terminal windows, or even completely different computers and they will be able to communicate! @@ -86,15 +84,38 @@ And since pub/sub allows any number of publishers and any number of subscriber per topic, you can start the same scripts more than once and the messages will be received and send multiple times! -Add typing information to messages ----------------------------------- +## Add typing information to messages So far, we have defined our messages as simple dictionaries. It is also possible to define a class that messages need to comform to, in order to get typing information on the messages. -.. literalinclude :: examples/04_message_type.py - :language: python +```python +--8<-- "docs/examples/04_message_type.py" +``` This example also shows how to set a default transport so that it does not need to be specified on every publisher and subscriber instance. + +## Distributed communication with Zenoh + +Just like the MQTT example above, we can achieve distributed communication +using [Apache Zenoh](https://zenoh.io/) as transport. Zenoh is an open source +implementation of a very fast and efficient protocol built around the idea of +Zero Overhead Network Protocol. + +The syntax remains almost identical, demonstrating how `compas_eve` abstracts +away the underlying transport layers. You only need to change the transport +initialization. + +First, let's look at the publisher using Zenoh: + +```python +--8<-- "docs/examples/05_zenoh_distributed_world_pub.py" +``` + +Next, we create the matching subscriber: + +```python +--8<-- "docs/examples/05_zenoh_distributed_world_sub.py" +``` diff --git a/docs/grasshopper.rst b/docs/grasshopper.md similarity index 60% rename from docs/grasshopper.rst rename to docs/grasshopper.md index c5231eb9..3531aece 100644 --- a/docs/grasshopper.rst +++ b/docs/grasshopper.md @@ -1,38 +1,29 @@ -******************************************************************************** -Examples in Rhino/Grasshopper -******************************************************************************** +# Grasshopper Integration -.. currentmodule:: compas_eve - -.. note:: - - This tutorial assumes that you have already installed ``compas_eve``. - If you haven't, please follow the instructions in the :ref:`installation` section. +!!! note + This tutorial assumes that you have already installed `compas_eve`. + If you haven't, please follow the instructions in the [installation](installation.md) section. **COMPAS EVE** provides tools to work with events inside Rhino/Grasshopper, as well as the ability to run long-running tasks in the background, which would otherwise block the UI. -Long-running tasks ------------------- +## Long-running tasks A long-running task is any snippet of code that takes a long time to execute. Normally, this would freeze the Grasshopper user interface. **COMPAS EVE** provides a mechanism to run such tasks in the background, so that the user can continue working with Grasshopper while the task is running. -In order to use it, add a ``Background task`` component to your Grasshopper definition, and connect +In order to use it, add a `Background task` component to your Grasshopper definition, and connect an input with a python function containing the code that needs to run in the background. The only -requirement is that this function must accept a ``worker`` argument, which is an instance of -:class:`~compas_eve.ghpython.BackgroundWorker`. +requirement is that this function must accept a `worker` argument, which is an instance of +[BackgroundWorker][compas_eve.ghpython.BackgroundWorker]. -.. figure:: /_images/background-task.png - :figclass: figure - :class: figure-img img-fluid +![PubSub](_images/background-task.png) The following code exemplifies how to use it to create a simple background task that generates a list of random values. The function adds some delay to simulate a long-running task. -.. code-block:: python - +```python import time import random @@ -47,7 +38,7 @@ a list of random values. The function adds some delay to simulate a long-running worker.display_message("Done!") return result - +``` It is also possible to update the results during the execution of the task. The result can be of any type, in the previous example it was a list of numbers. @@ -56,8 +47,7 @@ In the following example, the code generates a list of randomly placed Rhino poi and continuously updates the results as the list grows. The points will appear in the Rhino Viewport even before the task has completed. -.. code-block:: python - +```python import time import random import Rhino.Geometry as rg @@ -74,3 +64,17 @@ in the Rhino Viewport even before the task has completed. worker.display_message("Done!") return result +``` + +## Components + +The following components are available in Grasshopper: + +| Icon | Component | Description | +| :---: | --------- | ----------- | +| ![](_images/mqtt_connect.png) | `MqttConnect` | Connects to an MQTT broker. | +| ![](_images/zenoh_connect.png) | `ZenohConnect` | Connects to a Zenoh router. | +| ![](_images/message.png) | `Message` | Creates a new `compas_eve` message. | +| ![](_images/publish.png) | `Publish` | Publishes a message to a specific topic. | +| ![](_images/subscribe.png) | `Subscribe` | Subscribes to a specific topic. | +| ![](_images/background_task.png) | `BackgroundTask` | Runs a function continuously in the background. | diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..009fb604 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +# Event Extensions for COMPAS + +`compas_eve` adds event-based communication infrastructure to the COMPAS framework. +Using events is a way to decouple the components of a system, making it easier to develop, test, and maintain. + +![PubSub](_images/pubsub.png) + +```pycon +>>> import compas_eve as eve +>>> pub = eve.Publisher("/hello_world") +>>> sub = eve.Subscriber("/hello_world", print) +>>> sub.subscribe() +>>> for i in range(10): +... pub.publish(dict(text=f"Hello World {i}")) +``` diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 20950188..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,44 +0,0 @@ -******************************************************************************** -Event Extensions for COMPAS -******************************************************************************** - -.. rst-class:: lead - -``compas_eve`` adds event-based communication infrastructure to the COMPAS framework. - -Using events is a way to decouple the components of a system, making it easier to develop, test, and maintain. - -.. figure:: /_images/pubsub.png - :figclass: figure - :class: figure-img img-fluid - - -.. code-block:: python - - >>> import compas_eve as eve - >>> pub = eve.Publisher("/hello_world") - >>> sub = eve.EchoSubscriber("/hello_world") - >>> sub.subscribe() - >>> for i in range(10): - ... pub.publish(dict(text=f"Hello World {i}")) - -Table of Contents -================= - -.. toctree:: - :maxdepth: 2 - :titlesonly: - - Introduction - installation - examples - grasshopper - api - license - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..d58ae469 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,97 @@ +# Installation + +This chapter provides a step-by-step guide for installing `compas_eve` on your system. +We highly recommend using [uv](https://docs.astral.sh/uv/) for managing +your Python environment and dependencies, as it is significantly faster and +more reliable. Alternatively, you can simply use standard `pip` or `conda`. + +## Install uv + +If you do not have `uv` installed, follow the instructions on their website or run: + +=== "Mac/Linux" + + ```bash + curl -LsSf https://astral.sh/uv/install.sh | sh + ``` + +=== "Windows" + + ```powershell + powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + ``` + +## Create a virtual environment + +It is best practice to install `compas_eve` in a virtual environment. +Navigate to your project directory and run: + +```bash +uv venv +``` + +This creates a virtual environment in `.venv`. Activate it with: + +=== "Mac/Linux" + + ```bash + source .venv/bin/activate + ``` + +=== "Windows" + + ```powershell + .venv\Scripts\activate + ``` + +## Install compas_eve + +With your virtual environment activated, install `compas_eve`: + +```bash +uv pip install compas_eve +``` + +## Verify installation + +Verify that `compas_eve` is available: + +```bash +python -m compas_eve +``` + +If the version number is printed, the installation is complete. + +## Install for Rhino + +COMPAS EVE is compatible with Rhino 8 and later versions. + +### Rhino Script Editor + +To use `compas_eve` in a Python script, simply add the requirement header to the top of your script in the Rhino 8 Script Editor: + +```python +# r: compas_eve +``` +### Grasshopper + +To use `compas_eve` Grasshopper components, open the `Package Manager`, search for `compas_eve` and click `Install`. + +## Transports + +Depending on the transport you want to use, you might need to install additional dependencies. + +### MQTT Transport + +The `MQTT` transport is the default option and is installed automatically with `compas_eve`. + +### Zenoh Transport + +The `Zenoh` transport requires the `eclipse-zenoh` package, which is an optional dependency. +To install it, run: + +```bash +uv pip install compas_eve[zenoh] +``` + +For more details about Zenoh, refer to the [Eclipse Zenoh](https://zenoh.io/) website. diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index 6784cc5d..00000000 --- a/docs/installation.rst +++ /dev/null @@ -1,117 +0,0 @@ -******************************************************************************** -Installation -******************************************************************************** - -.. highlight:: bash - -**COMPAS EVE** can be easily installed on multiple platforms, -using popular package managers such as conda or pip. - -Install with conda -================== - -The recommended way to install **COMPAS EVE** is with `conda `_. -For example, create an environment named ``project_name`` and install ``compas_eve``. - -:: - - conda create -n project_name -c conda-forge compas_eve - -Afterwards, simply activate the environment and run the following -command to check if the installation process was successful. - -.. code-block:: bash - - conda activate project_name - python -m compas_eve - -.. code-block:: none - - COMPAS EVE v2.1.1 is installed! - -You are ready to use **COMPAS EVE**! - -Installation options --------------------- - -Install COMPAS EVE in an environment with a specific version of Python. - -.. code-block:: bash - - conda create -n project_name python=3.8 compas_eve - -Install COMPAS EVE in an existing environment. - -.. code-block:: bash - - conda install -n project_name compas_eve - -Install with pip -================ - -Install COMPAS EVE using ``pip`` from the Python Package Index. - -.. code-block:: bash - - pip install compas_eve - -Install an editable version from local source. - -.. code-block:: bash - - cd path/to/compas_eve - pip install -e . - -Note that installation with ``pip`` is also possible within a ``conda`` environment. - -.. code-block:: bash - - conda activate project_name - pip install -e . - - -Update with conda -================= - -To update COMPAS EVE to the latest version with ``conda`` - -.. code-block:: bash - - conda update compas_eve - -To switch to a specific version - -.. code-block:: bash - - conda install compas_eve=2.1.1 - - -Update with pip -=============== - -If you installed COMPAS EVE with ``pip`` the update command is the following - -.. code-block:: bash - - pip install --upgrade compas_eve - -Or to switch to a specific version - -.. code-block:: bash - - pip install compas_eve==2.1.1 - - -Working in Rhino -================ - -To make **COMPAS EVE** available inside Rhino, open the *command prompt*, -activate the appropriate environment, and type the following: - -:: - - python -m compas_rhino.install - -Open Rhino, start the Python script editor, type ``import compas_eve`` and -run it to verify that your installation is working. - diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 00000000..8187a4fc --- /dev/null +++ b/docs/license.md @@ -0,0 +1,3 @@ +# License + +--8<-- "LICENSE" \ No newline at end of file diff --git a/docs/license.rst b/docs/license.rst deleted file mode 100644 index e6a80ce0..00000000 --- a/docs/license.rst +++ /dev/null @@ -1,5 +0,0 @@ -******************************************************************************** -License -******************************************************************************** - -.. literalinclude:: ../LICENSE diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..688df3d4 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,145 @@ +site_name: COMPAS EVE +site_url: https://compas.dev/compas_eve +repo_url: https://github.com/compas-dev/compas_eve +repo_name: compas-dev/compas_eve +edit_uri: blob/main/docs/ + +copyright: Copyright © 2026, COMPAS Association + +extra: + homepage: https://compas.dev/compas_eve + version: + provider: mike + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Roboto + code: Roboto Mono + logo: _logo/compas_logo_white_transparent.png + favicon: _logo/favicon.ico + features: + - content.code.copy + - content.footnote.tooltips + - navigation.expand + - navigation.footer + - navigation.indexes + - navigation.sections + - navigation.top + - search.highlight + - search.suggest + - toc.follow + +extra_css: + - assets/stylesheets/custom.css + +markdown_extensions: + - abbr + - attr_list + - admonition + - callouts: + strip_period: no + - footnotes + - md_in_html + - pymdownx.blocks.caption + - pymdownx.caret + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.keys + - pymdownx.mark + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets: + check_paths: true + - toc: + permalink: "ยค" + - pymdownx.tabbed: + alternate_style: true + +plugins: + - search + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [src] # search packages in the src folder + inventories: + - https://docs.python.org/3/objects.inv + - https://compas.dev/compas/latest/objects.inv + options: + allow_inspection: true + backlinks: tree + docstring_options: + ignore_init_summary: true + trim_doctest_flags: true + docstring_style: numpy + docstring_section_style: list + filters: public + group_by_category: true + heading_level: 2 + inheritance_diagram_direction: TD + inherited_members: false + line_length: 88 + merge_init_into_class: true + modernize_annotations: true + parameter_headings: false + preload_modules: [mkdocstrings, compas] + relative_crossrefs: true + scoped_crossrefs: true + separate_signature: true + show_bases: false + show_category_heading: true + show_docstring_attributes: true + show_docstring_functions: true + show_docstring_modules: false + show_if_no_docstring: false + show_inheritance_diagram: false + show_root_heading: true + show_root_full_path: true + show_signature: true + show_signature_annotations: true + show_signature_type_parameters: true + show_source: false + show_submodules: false + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + summary: + modules: false + type_parameter_headings: true + unwrap_annotated: true + +nav: + - Home: index.md + - Installation: installation.md + - Examples: examples.md + - Grasshopper: grasshopper.md + - API Reference: + - compas_eve: api/compas_eve.md + - compas_eve.codecs: api/compas_eve.codecs.md + - compas_eve.memory: api/compas_eve.memory.md + - compas_eve.mqtt: api/compas_eve.mqtt.md + - compas_eve.zenoh: api/compas_eve.zenoh.md + - compas_eve.ghpython: api/compas_eve.ghpython.md + - License: license.md \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 1772fe59..ba466395 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ attrs >=17.4 black >=22.12.0 build bump-my-version -compas_invocations2 +compas_invocations2[mkdocs] compas_pb >= 0.4.4 invoke >=0.14 pytest-mock @@ -10,4 +10,4 @@ ruff sphinx_compas2_theme twine wheel -pythonnet \ No newline at end of file +pythonnet diff --git a/src/compas_eve/__init__.py b/src/compas_eve/__init__.py index 347552ec..0d3d2657 100644 --- a/src/compas_eve/__init__.py +++ b/src/compas_eve/__init__.py @@ -1,38 +1,5 @@ -""" -******************************************************************************** -compas_eve -******************************************************************************** - -.. currentmodule:: compas_eve - - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - Message - Topic - Publisher - Subscriber - EchoSubscriber - Transport - InMemoryTransport - MessageCodec - get_default_transport - set_default_transport - -""" - -from __future__ import print_function - -import os - - __author__ = ["Gonzalo Casas"] -__copyright__ = "Gramazio Kohler Research" +__copyright__ = "COMPAS Association" __license__ = "MIT License" __email__ = "casas@arch.ethz.ch" __version__ = "2.1.1" @@ -51,13 +18,9 @@ from .codecs import MessageCodec from .memory import InMemoryTransport -HERE = os.path.dirname(__file__) -HOME = os.path.abspath(os.path.join(HERE, "../../")) - set_default_transport(InMemoryTransport()) __all__ = [ - "HOME", "Message", "Publisher", "Subscriber", @@ -69,4 +32,3 @@ "set_default_transport", "InMemoryTransport", ] -__all_plugins__ = ["compas_eve.rhino.install"] diff --git a/src/compas_eve/codecs/__init__.py b/src/compas_eve/codecs/__init__.py index f23effef..99728395 100644 --- a/src/compas_eve/codecs/__init__.py +++ b/src/compas_eve/codecs/__init__.py @@ -1,26 +1,19 @@ -""" -******************************************************************************** -compas_eve.codecs -******************************************************************************** - -.. currentmodule:: compas_eve.codecs +from typing import Any +from typing import Optional +from typing import Union +from compas.data import json_dumps +from compas.data import json_loads -Classes -======= +from compas_eve.core import Message -.. autosummary:: - :toctree: generated/ - :nosignatures: +try: + import compas_pb - MessageCodec - JsonMessageCodec - ProtobufMessageCodec + COMPAS_PB_AVAILABLE = True +except ImportError: + COMPAS_PB_AVAILABLE = False -""" - -from compas.data import json_dumps -from compas.data import json_loads __all__ = ["MessageCodec", "JsonMessageCodec", "ProtobufMessageCodec"] @@ -32,12 +25,12 @@ class MessageCodec(object): to/from a specific representation format (e.g., JSON, Protocol Buffers). """ - def encode(self, message): + def encode(self, message: Union[Message, dict, Any]) -> Union[bytes, str]: """Encode a message to the codec's representation format. Parameters ---------- - message : :class:`Message` or dict or object + message Message to encode. Can be a Message instance, a dict, or an object implementing the COMPAS data framework. @@ -48,17 +41,17 @@ def encode(self, message): """ raise NotImplementedError("Subclasses must implement encode()") - def decode(self, encoded_data): + def decode(self, encoded_data: bytes) -> Union[Message, dict, Any]: """Decode data from the codec's representation format. Parameters ---------- - encoded_data : bytes + encoded_data Encoded data to decode. Returns ------- - :class:`Message` or dict or object + Union[Message, dict, Any] Decoded message after reconstruction from the encoded data. """ raise NotImplementedError("Subclasses must implement decode()") @@ -72,12 +65,12 @@ class JsonMessageCodec(MessageCodec): COMPAS Data objects, and regular dictionaries. """ - def encode(self, message): + def encode(self, message: Union[Message, dict, Any]) -> str: """Encode a message to JSON string. Parameters ---------- - message : :class:`Message` or dict or object + message Message to encode. Can be a Message instance, a dict, or an object implementing the COMPAS data framework. @@ -95,19 +88,19 @@ def encode(self, message): except (KeyError, AttributeError): return json_dumps(dict(message)) - def decode(self, encoded_data, message_type): + def decode(self, encoded_data: bytes, message_type: type) -> Message: """Decode JSON message payloads to message object. Parameters ---------- - encoded_data : bytes + encoded_data Message bytes to decode into a JSON string. - message_type : type + message_type The message type class to use for parsing. Returns ------- - :class:`Message` + Message Decoded message object. """ data = json_loads(encoded_data.decode()) @@ -117,14 +110,6 @@ def decode(self, encoded_data, message_type): return message_type.parse(data) -try: - import compas_pb - - COMPAS_PB_AVAILABLE = True -except ImportError: - COMPAS_PB_AVAILABLE = False - - class ProtobufMessageCodec(MessageCodec): """Protocol Buffers codec for message serialization. @@ -133,9 +118,9 @@ class ProtobufMessageCodec(MessageCodec): Note ---- - This codec requires the ``compas_pb`` package to be installed. - If ``compas_pb`` is not available, attempting to encode or decode - will raise an ImportError. + This codec requires the `compas_pb` package to be installed. + If `compas_pb` is not available, attempting to encode or decode + will raise an [ImportError][]. """ def __init__(self): @@ -143,12 +128,12 @@ def __init__(self): if not COMPAS_PB_AVAILABLE: raise ImportError("The ProtobufMessageCodec requires 'compas_pb' to be installed. Please install it with: pip install compas_pb") - def encode(self, message): + def encode(self, message: Union[Message, dict, Any]) -> bytes: """Encode a message to Protocol Buffers binary format. Parameters ---------- - message : :class:`Message` or dict or object + message Message to encode. Can be a Message instance, a dict, or an object implementing the COMPAS data framework. @@ -161,14 +146,14 @@ def encode(self, message): raise ImportError("The ProtobufMessageCodec requires 'compas_pb' to be installed. Please install it with: pip install compas_pb") return compas_pb.pb_dump_bts(message) - def decode(self, encoded_data, message_type=None): + def decode(self, encoded_data: bytes, message_type: Optional[type] = None) -> object: """Decode Protocol Buffers binary data to message object. Parameters ---------- - encoded_data : bytes + encoded_data Protocol Buffers binary data to decode. - message_type : type, optional + message_type The message type class (not used for protobuf as it's encoded in the data). Returns diff --git a/src/compas_eve/core.py b/src/compas_eve/core.py index ee1241c7..47d6445f 100644 --- a/src/compas_eve/core.py +++ b/src/compas_eve/core.py @@ -1,28 +1,34 @@ -from compas_eve.codecs import JsonMessageCodec +from typing import Any +from typing import Callable +from typing import Dict +from typing import Optional +from typing import Type +from typing import Union + DEFAULT_TRANSPORT = None -def get_default_transport(): +def get_default_transport() -> Optional["Transport"]: """Retrieve the default transport implementation to be used system-wide. Returns ------- - :class:`~compas_eve.Transport` - Instance of a transport class. By default, ``compas_eve`` uses - :class:`~compas_eve.memory.InMemoryTransport`. + Transport + Instance of a transport class. By default, `compas_eve` uses + [InMemoryTransport][compas_eve.memory.InMemoryTransport]. """ return DEFAULT_TRANSPORT -def set_default_transport(transport): +def set_default_transport(transport: Optional["Transport"]) -> None: """Assign a default transport implementation to be used system-wide. Parameters ---------- - transport : :class:`~compas_eve.Transport` - Instance of a transport class. By default, ``compas_eve`` uses - :class:`~compas_eve.memory.InMemoryTransport`. + transport + Instance of a transport class. By default, `compas_eve` uses + [InMemoryTransport][compas_eve.memory.InMemoryTransport]. """ global DEFAULT_TRANSPORT DEFAULT_TRANSPORT = transport @@ -33,37 +39,39 @@ class Transport(object): Parameters ---------- - codec : :class:`MessageCodec`, optional + codec The codec to use for encoding and decoding messages. - If not provided, defaults to :class:`JsonMessageCodec`. + If not provided, defaults to [JsonMessageCodec][compas_eve.codecs.JsonMessageCodec]. """ - def __init__(self, codec=None, *args, **kwargs): + def __init__(self, codec: Optional[Any] = None, *args: Any, **kwargs: Any) -> None: super(Transport, self).__init__(*args, **kwargs) + from compas_eve.codecs import JsonMessageCodec + self._id_counter = 0 if codec is None: codec = JsonMessageCodec() self.codec = codec @property - def id_counter(self): + def id_counter(self) -> int: """Generate an auto-incremental ID starting from 1.""" self._id_counter += 1 return self._id_counter - def publish(self, topic, message): + def publish(self, topic: "Topic", message: Union["Message", dict]) -> None: pass - def subscribe(self, topic, callback): + def subscribe(self, topic: "Topic", callback: Callable) -> Optional[str]: pass - def unsubscribe(self, topic): + def unsubscribe(self, topic: "Topic") -> None: pass - def advertise(self, topic): + def advertise(self, topic: "Topic") -> Optional[str]: pass - def unadvertise(self, topic): + def unadvertise(self, topic: "Topic") -> None: pass @@ -72,37 +80,37 @@ class Message(object): A message is fundamentally a dictionary and behaves as one.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super(Message, self).__init__() self.data = {} self.data.update(*args, **kwargs) - def ToString(self): + def ToString(self) -> str: return str(self) - def __str__(self): + def __str__(self) -> str: return str(self.data) - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: return self.data[name] - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key == "data" or key in self.__dict__: super(Message, self).__setattr__(key, value) else: self.data[key] = value - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: return self.data[key] - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self.data[key] = value - def __jsondump__(self, minimal=False): + def __jsondump__(self, minimal: bool = False) -> Dict[str, Any]: return self.data @classmethod - def parse(cls, value): + def parse(cls, value: Dict[str, Any]) -> "Message": instance = cls(**value) return instance @@ -115,19 +123,19 @@ class Topic(object): Attributes ---------- - name : str + name Name of the topic. - message_type : type - Class defining the message structure. Use :class:`Message` for + message_type + Class defining the message structure. Use [Message][] for a generic, non-typed checked message implementation. - Defaults to :class:`Message`. - options : dict + Defaults to [Message][]. + options A dictionary of options. """ # TODO: Add documentation/examples of possible options - def __init__(self, name, message_type=None, **options): + def __init__(self, name: str, message_type: Optional[Type[Message]] = None, **options: Any) -> None: self.name = name self.message_type = message_type or Message self.options = options @@ -138,20 +146,20 @@ class Publisher(object): Parameters ---------- - topic : :class:`Topic` or str + topic The topic to publish messages to. If a string is provided, a new topic instance will be created using the string as topic name. - transport : :class:`Transport`, optional + transport The transport to use for publishing. If not provided, the default transport will be used. """ - def __init__(self, topic, transport=None): + def __init__(self, topic: Union[Topic, str], transport: Optional[Transport] = None) -> None: self.topic = topic if isinstance(topic, Topic) else Topic(topic) self.transport = transport or get_default_transport() self._advertise_id = None @property - def is_advertised(self): + def is_advertised(self) -> bool: """Indicate if the publisher has announced its topic as advertised or not. Returns @@ -161,16 +169,16 @@ def is_advertised(self): """ return self._advertise_id is not None - def message_published(self, message): + def message_published(self, message: Union[Message, dict]) -> None: """Handler called when a message has been published.""" pass - def publish(self, message): + def publish(self, message: Union[Message, dict]) -> None: """Publish a message to the topic. Parameters ---------- - message : :class:`Message` or dict + message The message to publish. """ # TODO: check if message type matches self.topic.message_type declared @@ -180,14 +188,14 @@ def publish(self, message): self.transport.publish(self.topic, message) self.message_published(message) - def advertise(self): + def advertise(self) -> None: """Advertise the publisher for the topic.""" if self.is_advertised: return self._advertise_id = self.transport.advertise(self.topic) - def unadvertise(self): + def unadvertise(self) -> None: """Unadvertise the publisher for the topic.""" if not self.is_advertised: return @@ -201,20 +209,20 @@ class Subscriber(object): Parameters ---------- - topic : :class:`Topic` or str + topic The topic to subscribe to. If a string is provided, a new topic instance will be created using the string as topic name. - transport : :class:`Transport`, optional + transport The transport to use for subscribing. If not provided, the default transport will be used. """ - def __init__(self, topic, callback=None, transport=None): + def __init__(self, topic: Union[Topic, str], callback: Optional[Callable] = None, transport: Optional[Transport] = None) -> None: self.transport = transport or get_default_transport() self.topic = topic if isinstance(topic, Topic) else Topic(topic) self._subscribe_id = None self._callback = callback - def message_received(self, message): + def message_received(self, message: Union[Message, dict]) -> None: """Handler called whenever a new message is received. By default, this implementation will simply invoke the callback @@ -223,17 +231,17 @@ def message_received(self, message): self._callback(message) @property - def is_subscribed(self): + def is_subscribed(self) -> bool: """Indicate if the instace is currently subscribed to its topic or not.""" return self._subscribe_id is not None - def subscribe(self): + def subscribe(self) -> None: if self._subscribe_id: return self._subscribe_id = self.transport.subscribe(self.topic, self.message_received) - def unsubscribe(self): + def unsubscribe(self) -> None: """Unregister the subscriber from its topic.""" if not self._subscribe_id: return @@ -247,14 +255,14 @@ class EchoSubscriber(Subscriber): Parameters ---------- - topic : :class:`Topic` or str + topic The topic to subscribe to. If a string is provided, a new topic instance will be created using the string as topic name. """ - def __init__(self, topic, transport=None): + def __init__(self, topic: Union[Topic, str], transport: Optional[Transport] = None) -> None: super(EchoSubscriber, self).__init__(topic, callback=self.echo, transport=transport) - def echo(self, message): + def echo(self, message: Union[Message, dict]) -> None: """Print received messages to the console.""" print(str(message)) diff --git a/src/compas_eve/ghpython/__init__.py b/src/compas_eve/ghpython/__init__.py index ad054b8b..c78e8382 100644 --- a/src/compas_eve/ghpython/__init__.py +++ b/src/compas_eve/ghpython/__init__.py @@ -1,77 +1,58 @@ -""" -******************************************************************************** -compas_eve.ghpython -******************************************************************************** - -.. currentmodule:: compas_eve.ghpython - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - BackgroundWorker - -""" - try: import Grasshopper # type: ignore except (ImportError, SyntaxError): pass - from .background import BackgroundWorker -def warning(component, message): +def warning(component: "Grasshopper.Kernel.IGH_Component", message: str): """Add a warning message to the component. Parameters ---------- - component : Grasshopper.Kernel.IGH_Component - The component instance. Pre-Rhino8 use `self`. Post-Rhino8 use `ghenv.Component`. - message : str + component + The component instance. Use `ghenv.Component`. + message The message to display. """ component.AddRuntimeMessage(Grasshopper.Kernel.GH_RuntimeMessageLevel.Warning, message) -def error(component, message): +def error(component: "Grasshopper.Kernel.IGH_Component", message: str): """Add an error message to the component. Parameters ---------- - component : Grasshopper.Kernel.IGH_Component + component The component instance. Pre-Rhino8 use `self`. Post-Rhino8 use `ghenv.Component`. - message : str + message The message to display. """ component.AddRuntimeMessage(Grasshopper.Kernel.GH_RuntimeMessageLevel.Error, message) -def remark(component, message): +def remark(component: "Grasshopper.Kernel.IGH_Component", message: str): """Add a remark message to the component. Parameters ---------- - component : Grasshopper.Kernel.IGH_Component + component The component instance. Pre-Rhino8 use `self`. Post-Rhino8 use `ghenv.Component`. - message : str + message The message to display. """ component.AddRuntimeMessage(Grasshopper.Kernel.GH_RuntimeMessageLevel.Remark, message) -def message(component, message): +def message(component: "Grasshopper.Kernel.IGH_Component", message: str): """Add a text that will appear under the component. Parameters ---------- - component : Grasshopper.Kernel.IGH_Component + component The component instance. Pre-Rhino8 use `self`. Post-Rhino8 use `ghenv.Component`. - message : str + message The message to display. """ component.Message = message diff --git a/src/compas_eve/ghpython/background.py b/src/compas_eve/ghpython/background.py index e72fc8aa..b2d64f81 100644 --- a/src/compas_eve/ghpython/background.py +++ b/src/compas_eve/ghpython/background.py @@ -1,19 +1,17 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import threading +from typing import Any +from typing import Callable +from typing import Optional -import Rhino -import scriptcontext -import System -from compas_ghpython import create_id - -# COMPAS 1.x compatibility try: + import GhPython + import Rhino + import scriptcontext + import System + from compas_ghpython import create_id from compas_ghpython.timer import update_component except ImportError: - from compas_ghpython import update_component + pass class BackgroundWorker(object): @@ -29,43 +27,49 @@ class BackgroundWorker(object): The following is an example of a long-running function that updates the progress while it runs. - .. code-block:: python - - import time - + ```python + import time - def do_something_long_and_complicated(worker): - # Result can be of any data type - result = 0 - for i in range(50): - worker.current_value = i - result += i - worker.display_progress(i / (50 - 1)) - time.sleep(0.01) + def do_something_long_and_complicated(worker): + # Result can be of any data type + result = 0 - worker.display_message("Done!") + for i in range(50): + worker.current_value = i + result += i + worker.display_progress(i / (50 - 1)) + time.sleep(0.01) - return result + worker.display_message("Done!") + return result + ``` Parameters ---------- - ghenv : ``GhPython.Component.PythonEnvironment`` + ghenv Grasshopper environment object - long_running_function : function, optional + long_running_function This function will be the main entry point for the long-running task. - dispose_function : function, optional + dispose_function If defined, this function will be called when the worker is disposed. It can be used for clean-up tasks and resource deallocation. - auto_set_done : bool, optional - If true, the worker state will be automatically set to ``Done`` after the function returns. - Defaults to ``True``. - args : tuple, optional - List or tuple of arguments for the invocation of the ``long_running_function``. Defaults to ``()``. + auto_set_done + If true, the worker state will be automatically set to `Done` after the function returns. + Defaults to `True`. + args + List or tuple of arguments for the invocation of the `long_running_function`. Defaults to `()`. """ - def __init__(self, ghenv, long_running_function=None, dispose_function=None, auto_set_done=True, args=()): + def __init__( + self, + ghenv: "GhPython.Component.PythonEnvironment", + long_running_function: Optional[Callable] = None, + dispose_function: Optional[Callable] = None, + auto_set_done: bool = True, + args=(), + ): super(BackgroundWorker, self).__init__() self.ghenv = ghenv self._is_working = False @@ -130,19 +134,19 @@ def dispose(self): self.dispose_function(self) def set_internal_state_to_working(self): - """Set the internal state to ``working``.""" + """Set the internal state to `working`.""" self._is_working = True self._is_done = False self._is_cancelled = False - def set_internal_state_to_done(self, result): - """Set the internal state to ``done``, which indicates the worker has completed.""" + def set_internal_state_to_done(self, result: Any): + """Set the internal state to `done`, which indicates the worker has completed.""" self._is_working = False self._is_done = True self._is_cancelled = False self.update_result(result, delay=1) - def update_result(self, result, delay=1): + def update_result(self, result: Any, delay: int = 1): """Update the result of the worker. This will update the result of the worker, and trigger a solution expiration @@ -150,36 +154,36 @@ def update_result(self, result, delay=1): Parameters ---------- - result : object + result Result of the worker. - delay : int, optional + delay Delay (in milliseconds) before updating the component, by default 1. """ self.result = result update_component(self.ghenv, delay) def set_internal_state_to_cancelled(self): - """Set the internal state to ``cancelled``.""" + """Set the internal state to `cancelled`.""" self._is_working = False self._is_done = False self._is_cancelled = True - def display_progress(self, progress): + def display_progress(self, progress: float): """Display a progress indicator in the component. Parameters ---------- - progress : float - Float between ``0..1`` indicating progress of completion. + progress + Float between `0..1` indicating progress of completion. """ self.display_message("Progress {:.1f}%".format(progress * 100)) - def display_message(self, message): + def display_message(self, message: str): """Display a message in the component without triggering a solution expiration. Parameters ---------- - message : str + message Message to display. """ @@ -191,7 +195,15 @@ def ui_callback(): Rhino.RhinoApp.InvokeOnUiThread(System.Action(ui_callback)) @classmethod - def instance_by_component(cls, ghenv, long_running_function=None, dispose_function=None, auto_set_done=True, force_new=False, args=()): + def instance_by_component( + cls, + ghenv: "GhPython.Component.PythonEnvironment", + long_running_function: Optional[Callable] = None, + dispose_function: Optional[Callable] = None, + auto_set_done: bool = True, + force_new: bool = False, + args=(), + ): """Get the worker instance assigned to the component. This will get a persistant instance of a background worker @@ -200,24 +212,24 @@ def instance_by_component(cls, ghenv, long_running_function=None, dispose_functi Parameters ---------- - ghenv : ``GhPython.Component.PythonEnvironment`` + ghenv Grasshopper environment object - long_running_function : function, optional + long_running_function This function will be the main entry point for the long-running task. - dispose_function : function, optional + dispose_function If defined, this function will be called when the worker is disposed. It can be used for clean-up tasks and resource deallocation. - auto_set_done : bool, optional - If true, the worker state will be automatically set to ``Done`` after the function returns. - Defaults to ``True``. - force_new : bool, optional - Force the creation of a new background worker, by default False. + auto_set_done + If true, the worker state will be automatically set to `Done` after the function returns. + Defaults to `True`. + force_new + Force the creation of a new background worker, by default `False`. args : tuple, optional - List or tuple of arguments for the invocation of the ``long_running_function``. Defaults to ``()``. + List or tuple of arguments for the invocation of the `long_running_function`. Defaults to `()`. Returns ------- - :class:`BackgroundWorker` + BackgroundWorker Instance of the background worker of the current component. """ @@ -243,14 +255,14 @@ def instance_by_component(cls, ghenv, long_running_function=None, dispose_functi return worker @classmethod - def stop_instance_by_component(cls, ghenv): + def stop_instance_by_component(cls, ghenv: "GhPython.Component.PythonEnvironment"): """Stops the worker instance assigned to the component. If there is no worker running, it will do nothing. Parameters ---------- - ghenv : ``GhPython.Component.PythonEnvironment`` + ghenv Grasshopper environment object """ diff --git a/src/compas_eve/memory/__init__.py b/src/compas_eve/memory/__init__.py index 4a40f509..58b070c4 100644 --- a/src/compas_eve/memory/__init__.py +++ b/src/compas_eve/memory/__init__.py @@ -1,24 +1,10 @@ -""" -******************************************************************************** -compas_eve.memory -******************************************************************************** - -.. currentmodule:: compas_eve.memory - - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - InMemoryTransport - -""" - +from typing import Callable +from typing import Optional +from compas_eve.codecs import MessageCodec from compas_eve.event_emitter import EventEmitterMixin from compas_eve.core import Transport +from compas_eve.core import Topic +from compas_eve.core import Message __all__ = ["InMemoryTransport"] @@ -30,27 +16,27 @@ class InMemoryTransport(Transport, EventEmitterMixin): Parameters ---------- - codec : :class:`MessageCodec`, optional + codec The codec to use for encoding and decoding messages. - If not provided, defaults to :class:`JsonMessageCodec`. + If not provided, defaults to [JsonMessageCodec][compas_eve.codecs.JsonMessageCodec]. """ - def __init__(self, codec=None, *args, **kwargs): + def __init__(self, codec: Optional[MessageCodec] = None, *args, **kwargs): super(InMemoryTransport, self).__init__(codec=codec, *args, **kwargs) self._local_callbacks = {} - def on_ready(self, callback): + def on_ready(self, callback: Callable): """In-memory transport is always ready, it will immediately trigger the callback.""" callback() - def publish(self, topic, message): + def publish(self, topic: Topic, message: Message): """Publish a message to a topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to publish to. - message : :class:`Message` + message Instance of the message to publish. """ event_key = "event:{}".format(topic.name) @@ -62,18 +48,18 @@ def _callback(**kwargs): self.on_ready(_callback) - def subscribe(self, topic, callback): + def subscribe(self, topic: Topic, callback: Callable) -> str: """Subscribe to a topic. Every time a new message is received on the topic, the callback will be invoked. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to subscribe to. - callback : function + callback Callback to invoke whenever a new message arrives. The callback should - receive only one `msg` argument, e.g. ``lambda msg: print(msg)``. + receive only one `msg` argument, e.g. `lambda msg: print(msg)`. Returns ------- @@ -96,7 +82,7 @@ def _callback(**kwargs): return subscribe_id - def unsubscribe_by_id(self, subscribe_id): + def unsubscribe_by_id(self, subscribe_id: str): """Unsubscribe from the specified topic based on the subscription id.""" ev_type, topic_name, _callback_id = subscribe_id.split(":") event_key = "{}:{}".format(ev_type, topic_name) @@ -105,12 +91,19 @@ def unsubscribe_by_id(self, subscribe_id): self.off(event_key, callback) del self._local_callbacks[subscribe_id] - def unsubscribe(self, topic): - """Unsubscribe from the specified topic.""" + def unsubscribe(self, topic: Topic): + """Unsubscribe from the specified topic. + + + Parameters + ---------- + topic + Instance of the topic to unsubscribe from. + """ event_key = "event:{}".format(topic.name) self.remove_all_listeners(event_key) - def advertise(self, topic): + def advertise(self, topic: Topic): """Announce this code will publish messages to the specified topic. This call has no effect on the in-memory transport.""" @@ -118,7 +111,7 @@ def advertise(self, topic): # in-memory does not need anything here return advertise_id - def unadvertise(self, topic): + def unadvertise(self, topic: Topic): """Announce that this code will stop publishing messages to the specified topic. This call has no effect on the in-memory transport.""" diff --git a/src/compas_eve/mqtt/__init__.py b/src/compas_eve/mqtt/__init__.py index 6a40e2b1..cc426ec9 100644 --- a/src/compas_eve/mqtt/__init__.py +++ b/src/compas_eve/mqtt/__init__.py @@ -1,22 +1,3 @@ -""" -******************************************************************************** -compas_eve.mqtt -******************************************************************************** - -.. currentmodule:: compas_eve.mqtt - - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - MqttTransport - -""" - from .mqtt_paho import MqttTransport __all__ = ["MqttTransport"] diff --git a/src/compas_eve/mqtt/mqtt_paho.py b/src/compas_eve/mqtt/mqtt_paho.py index a154e723..1aac3292 100644 --- a/src/compas_eve/mqtt/mqtt_paho.py +++ b/src/compas_eve/mqtt/mqtt_paho.py @@ -1,7 +1,12 @@ import uuid +from typing import Callable +from typing import Optional import paho.mqtt.client as mqtt +from ..codecs import MessageCodec +from ..core import Message +from ..core import Topic from ..core import Transport from ..event_emitter import EventEmitterMixin @@ -18,19 +23,19 @@ class MqttTransport(Transport, EventEmitterMixin): Parameters ---------- - host : str - Host name for the MQTT broker, e.g. ``broker.hivemq.com`` or ``localhost`` if + host + Host name for the MQTT broker, e.g. `broker.hivemq.com` or `localhost` if you are running a local broker on your machine. - port : int - MQTT broker port, defaults to ``1883``. - client_id : str, optional + port + MQTT broker port, defaults to `1883`. + client_id Client ID for the MQTT connection. If not provided, a unique ID will be generated. - codec : :class:`MessageCodec`, optional + codec The codec to use for encoding and decoding messages. - If not provided, defaults to :class:`JsonMessageCodec`. + If not provided, defaults to [JsonMessageCodec][compas_eve.codecs.JsonMessageCodec]. """ - def __init__(self, host, port=1883, client_id=None, codec=None, *args, **kwargs): + def __init__(self, host: str, port: int = 1883, client_id: Optional[str] = None, codec: Optional[MessageCodec] = None, *args, **kwargs): super(MqttTransport, self).__init__(codec=codec, *args, **kwargs) self.host = host self.port = port @@ -47,20 +52,20 @@ def __init__(self, host, port=1883, client_id=None, codec=None, *args, **kwargs) self.client.connect(self.host, self.port) self.client.loop_start() - def close(self): + def close(self) -> None: """Close the connection to the MQTT broker.""" self.client.loop_stop() - def _on_connect(self, client, userdata, flags, rc): + def _on_connect(self, client, userdata, flags, rc) -> None: self._is_connected = True self.emit("ready") - def on_ready(self, callback): + def on_ready(self, callback: Callable): """Allows to hook-up to the event triggered when the connection to MQTT broker is ready. Parameters ---------- - callback : function + callback Function to invoke when the connection is established. """ if self._is_connected: @@ -68,14 +73,14 @@ def on_ready(self, callback): else: self.once("ready", callback) - def publish(self, topic, message): + def publish(self, topic: Topic, message: Message): """Publish a message to a topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to publish to. - message : :class:`Message` + message Instance of the message to publish. """ @@ -85,18 +90,18 @@ def _callback(**kwargs): self.on_ready(_callback) - def subscribe(self, topic, callback): + def subscribe(self, topic: Topic, callback: Callable) -> str: """Subscribe to a topic. Every time a new message is received on the topic, the callback will be invoked. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to subscribe to. - callback : function + callback Callback to invoke whenever a new message arrives. The callback should - receive only one `msg` argument, e.g. ``lambda msg: print(msg)``. + receive only one `msg` argument, e.g. `lambda msg: print(msg)`. Returns ------- @@ -129,14 +134,14 @@ def _on_message(self, client, userdata, msg): event_key = "event:{}".format(msg.topic) self.emit(event_key, msg) - def advertise(self, topic): + def advertise(self, topic: Topic) -> str: """Announce this code will publish messages to the specified topic. This call has no effect on this transport implementation. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to advertise. Returns @@ -149,24 +154,24 @@ def advertise(self, topic): # mqtt does not need anything here return advertise_id - def unadvertise(self, topic): + def unadvertise(self, topic: Topic): """Announce that this code will stop publishing messages to the specified topic. This call has no effect on this transport implementation. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to stop publishing messages to. """ pass - def unsubscribe_by_id(self, subscribe_id): + def unsubscribe_by_id(self, subscribe_id: str): """Unsubscribe from the specified topic based on the subscription id. Parameters ---------- - subscribe_id : str + subscribe_id Identifier of the subscription. """ ev_type, topic_name, _callback_id = subscribe_id.split(":") @@ -178,12 +183,12 @@ def unsubscribe_by_id(self, subscribe_id): del self._local_callbacks[subscribe_id] - def unsubscribe(self, topic): + def unsubscribe(self, topic: Topic): """Unsubscribe from the specified topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to unsubscribe from. """ self.client.unsubscribe(topic.name) diff --git a/src/compas_eve/rhino/__init__.py b/src/compas_eve/rhino/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/compas_eve/rhino/install.py b/src/compas_eve/rhino/install.py deleted file mode 100644 index b3011de0..00000000 --- a/src/compas_eve/rhino/install.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import glob -import os - -import compas -import compas.plugins -from compas_ghpython.components import install_userobjects -from compas_ghpython.components import uninstall_userobjects - - -@compas.plugins.plugin(category="install") -def installable_rhino_packages(): - return ["compas_eve"] - - -@compas.plugins.plugin(category="install") -def after_rhino_install(installed_packages): - project = "compas_eve" - if project not in installed_packages: - return [] - - srcdir = os.path.join(os.path.dirname(__file__), "..", "ghpython", "components", "ghuser") - installed_objects = install_userobjects(srcdir) - msg = "Installed {} GH User Objects".format(len(installed_objects)) - - return [ - ( - project, - msg, - True, - ) - ] - - -@compas.plugins.plugin(category="install") -def after_rhino_uninstall(uninstalled_packages): - project = "compas_eve" - if project not in uninstalled_packages: - return [] - - srcdir = os.path.join(os.path.dirname(__file__), "..", "ghpython", "components", "ghuser") - userobjects = [os.path.basename(ghuser) for ghuser in glob.glob(os.path.join(srcdir, "*.ghuser"))] - uninstalled_objects = uninstall_userobjects(userobjects) - - uninstall_errors = [uo[0] for uo in uninstalled_objects if not uo[1]] - error_msg = "" if not uninstall_errors else "and {} failed to uninstall".format(len(uninstall_errors)) - msg = "Uninstalled {} GH User Objects {}".format(len(uninstalled_objects), error_msg) - - return [ - ( - project, - msg, - True, - ) - ] diff --git a/src/compas_eve/zenoh/__init__.py b/src/compas_eve/zenoh/__init__.py index 82f75b8a..1f317c70 100644 --- a/src/compas_eve/zenoh/__init__.py +++ b/src/compas_eve/zenoh/__init__.py @@ -1,22 +1,3 @@ -""" -******************************************************************************** -compas_eve.zenoh -******************************************************************************** - -.. currentmodule:: compas_eve.zenoh - - -Classes -======= - -.. autosummary:: - :toctree: generated/ - :nosignatures: - - ZenohTransport - -""" - from .zenoh_transport import ZenohTransport __all__ = ["ZenohTransport"] diff --git a/src/compas_eve/zenoh/zenoh_transport.py b/src/compas_eve/zenoh/zenoh_transport.py index 8307af41..288856e8 100644 --- a/src/compas_eve/zenoh/zenoh_transport.py +++ b/src/compas_eve/zenoh/zenoh_transport.py @@ -1,7 +1,13 @@ import threading +from typing import Any +from typing import Callable +from typing import Optional import zenoh +from ..codecs import MessageCodec +from ..core import Message +from ..core import Topic from ..core import Transport from ..event_emitter import EventEmitterMixin @@ -11,14 +17,14 @@ class ZenohTransport(Transport, EventEmitterMixin): Parameters ---------- - config : :class:`zenoh.Config`, optional + config The Zenoh configuration to use. If not provided, a default `zenoh.Config()` will be used. - codec : :class:`MessageCodec`, optional + codec The codec to use for encoding and decoding messages. - If not provided, defaults to :class:`JsonMessageCodec`. + If not provided, defaults to [JsonMessageCodec][compas_eve.codecs.JsonMessageCodec]. """ - def __init__(self, config=None, codec=None, *args, **kwargs): + def __init__(self, config: Optional[zenoh.Config] = None, codec: Optional[MessageCodec] = None, *args: Any, **kwargs: Any) -> None: super(ZenohTransport, self).__init__(codec=codec, *args, **kwargs) if config is None: self.config = zenoh.Config() @@ -33,24 +39,24 @@ def __init__(self, config=None, codec=None, *args, **kwargs): self.session = zenoh.open(self.config) self._is_connected = True - def emit_ready(): + def emit_ready() -> None: self.emit("ready") threading.Thread(target=emit_ready).start() - def close(self): + def close(self) -> None: """Close the Zenoh session.""" self.session.close() - def _get_topic_name(self, topic): + def _get_topic_name(self, topic: Topic) -> str: return topic.name.strip("/") - def on_ready(self, callback): + def on_ready(self, callback: Callable) -> None: """Allows to hook-up to the event triggered when the connection is established. Parameters ---------- - callback : function + callback Function to invoke when the connection is established. """ if self._is_connected: @@ -58,18 +64,18 @@ def on_ready(self, callback): else: self.once("ready", callback) - def publish(self, topic, message): + def publish(self, topic: Topic, message: Message) -> None: """Publish a message to a topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to publish to. - message : :class:`Message` + message Instance of the message to publish. """ - def _callback(**kwargs): + def _callback(**kwargs: Any) -> None: if self._get_topic_name(topic) not in self._publishers: self._publishers[self._get_topic_name(topic)] = self.session.declare_publisher(self._get_topic_name(topic)) @@ -78,33 +84,33 @@ def _callback(**kwargs): self.on_ready(_callback) - def subscribe(self, topic, callback): + def subscribe(self, topic: Topic, callback: Callable) -> str: """Subscribe to a topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to subscribe to. - callback : function + callback Callback to invoke whenever a new message arrives. Returns ------- str - Returns an identifier of the subscription. + Identifier of the subscription. """ event_key = "event:{}".format(self._get_topic_name(topic)) subscribe_id = "{}:{}".format(event_key, id(callback)) - def _local_callback(msg): + def _local_callback(msg: Any) -> None: callback(msg) - def _zenoh_handler(sample): + def _zenoh_handler(sample: Any) -> None: payload = sample.payload.to_bytes() if hasattr(sample.payload, "to_bytes") else bytes(sample.payload) message_obj = self.codec.decode(payload, topic.message_type) self.emit(event_key, message_obj) - def _subscribe_callback(**kwargs): + def _subscribe_callback(**kwargs: Any) -> None: if self._get_topic_name(topic) not in self._subscribers: self._subscribers[self._get_topic_name(topic)] = self.session.declare_subscriber(self._get_topic_name(topic), _zenoh_handler) @@ -116,12 +122,12 @@ def _subscribe_callback(**kwargs): return subscribe_id - def unsubscribe(self, topic): + def unsubscribe(self, topic: Topic) -> None: """Unsubscribe from a topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to unsubscribe from. """ event_key = "event:{}".format(self._get_topic_name(topic)) @@ -135,12 +141,12 @@ def unsubscribe(self, topic): self.remove_listener(event_key, self._local_callbacks[k]) del self._local_callbacks[k] - def advertise(self, topic): + def advertise(self, topic: Topic) -> str: """Announce this code will publish messages to the specified topic. Parameters ---------- - topic : :class:`Topic` + topic Instance of the topic to advertise. Returns @@ -151,12 +157,12 @@ def advertise(self, topic): advertise_id = "advertise:{}:{}".format(self._get_topic_name(topic), self.id_counter) return advertise_id - def unsubscribe_by_id(self, subscribe_id): + def unsubscribe_by_id(self, subscribe_id: str) -> None: """Unsubscribe from the specified topic based on the subscription id. Parameters ---------- - subscribe_id : str + subscribe_id The subscription identifier. """ # subscribe_id format: "event:topic_name:id(callback)" diff --git a/tasks.py b/tasks.py index 9cd84565..50ae0e3a 100644 --- a/tasks.py +++ b/tasks.py @@ -5,6 +5,7 @@ from compas_invocations2 import build from compas_invocations2 import docs +from compas_invocations2 import mkdocs from compas_invocations2 import style from compas_invocations2 import tests from compas_invocations2 import grasshopper @@ -18,7 +19,7 @@ style.check, style.lint, style.format, - docs.docs, + mkdocs.docs, docs.linkcheck, tests.test, tests.testdocs,