diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..35ee05d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish release + +on: + workflow_dispatch: + push: + tags: + - v** + +jobs: + docs: + name: Build and publish docs + runs-on: ubuntu-latest + needs: pypi + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Install dependencies + run: uv sync --group docs + - name: Build docs + run: mkdocs build -v + - name: Deploy gh-pages + uses: JamesIves/github-pages-deploy-action@v4.7.3 + with: + folder: site + # TODO: restructure in another PR + # - name: set version + # run: | + # VERSION=${{ env.VERSION }} + # - name: Create GitHub release entry + # uses: softprops/action-gh-release@v2 + # id: create_release + # with: + # draft: false + # prerelease: false + # name: ${{ env.VERSION }} + # tag_name: ${{ env.VERSION }} + # env: + # GITHUB_TOKEN: ${{ github.token }} diff --git a/Makefile b/Makefile index 31755d6..4ad6cbd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help sync install-ci mypy pylint pre-commit +.PHONY: help sync install-ci mypy pylint pre-commit docs-serve docs-deploy default: help @@ -36,3 +36,11 @@ pre-commit: ## check Run all code checks (mypy, pre-commit, pylint). check: mypy pre-commit pylint + +## docs-serve Serve documentation locally. +docs-serve: + uv run --active mkdocs serve + +## docs-deploy Deploy documentation to GitHub Pages. +docs-deploy: + uv run --active mkdocs gh-deploy --force && rm -rf site diff --git a/README.md b/README.md index 26eafdb..d0dea44 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -# Feldfreund DevKit +Feldfreund rendering -A sturdy development platform for autonomous outdoor robotics. +# Feldfreund Dev Kit -## Tech Stack +This is the source code of the [Feldfreund Dev Kit](https://zauberzeug.com/products/field-friend-dev-kit) for autonomous outdoor robotics made by [Zauberzeug](https://zauberzeug.com/). +The software is based on [RoSys](https://rosys.io) and [NiceGUI](https://nicegui.io/). +The micro controller is programmed with [Lizard](https://github.com/zauberzeug/lizard). -- Python 3.11+ -- [NiceGUI](https://nicegui.io) for web interface -- [uv](https://astral.sh/uv) for dependency management -- [RoSys](https://rosys.io) framework -- [Copier](https://copier.readthedocs.io/) for template configuration (from [nicegui-template](https://github.com/zauberzeug/nicegui-template)) +Our agricultural weeding robot [Feldfreund](https://zauberzeug.com/feldfreund) is based on this platform and is intended to advance organic and regenerative agriculture. +There is also a [ROS2 implementation](https://github.com/zauberzeug/feldfreund_devkit_ros) based on this repository. + +Please see the [documentation](https://docs.feldfreund.de) for details on installation, setup and usage. ## Development diff --git a/assets/feldfreund.webp b/assets/feldfreund.webp new file mode 100644 index 0000000..e056b44 Binary files /dev/null and b/assets/feldfreund.webp differ diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..095e7b6 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +docs.feldfreund.de diff --git a/docs/generate_reference.py b/docs/generate_reference.py new file mode 100644 index 0000000..99024fa --- /dev/null +++ b/docs/generate_reference.py @@ -0,0 +1,132 @@ +import importlib +import inspect +import logging +import re +import sys +from pathlib import Path +from types import ModuleType + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() + + +def extract_events(filepath: str) -> dict[str, str]: + with open(filepath, encoding='utf-8') as f: + lines = f.read().splitlines() + events_: dict[str, str] = {} + for i, line in enumerate(lines): + if re.search(r'= Event(\[.*?\])?\(\)$', line): + event_name_ = line.strip().split()[0].removeprefix('self.').rstrip(':') + event_doc_ = lines[i + 1].split('"""')[1] + events_[event_name_] = event_doc_ + return events_ + + +def format_type(hint) -> str: + if hint is None: + return '' + if hint is type(None): + return 'None' + if hasattr(hint, '__name__'): + return f'`{hint.__name__}`' + type_str = str(hint).replace('typing.', '').replace('NoneType', 'None') + return f'`{type_str}`' + + +def extract_properties(cls: type) -> dict[str, tuple[str, str]]: + props: dict[str, tuple[str, str]] = {} + for name, obj in inspect.getmembers(cls): + if name.startswith('_'): + continue + if isinstance(obj, property) and obj.fget is not None: + doc = obj.fget.__doc__ or '' + doc = doc.strip().split('\n')[0] if doc else '' + type_hint = obj.fget.__annotations__.get('return') + props[name] = (format_type(type_hint), doc) + return props + + +def extract_instance_attributes(filepath: str) -> dict[str, tuple[str, str]]: + """Extract instance attributes with their inline docstrings from source.""" + with open(filepath, encoding='utf-8') as f: + lines = f.read().splitlines() + attrs: dict[str, tuple[str, str]] = {} + for i, line in enumerate(lines): + match = re.match(r'\s+self\.(\w+)(?::\s*(\S+))?\s*=', line) + if match: + attr_name = match.group(1) + attr_type = match.group(2) or '' + if attr_name.startswith('_'): + continue + if i + 1 < len(lines) and '"""' in lines[i + 1]: + doc = lines[i + 1].split('"""')[1] + type_str = f'`{attr_type}`' if attr_type else '' + attrs[attr_name] = (type_str, doc) + return attrs + + +for path in sorted(Path('feldfreund_devkit').rglob('__init__.py')): + identifier = str(path.parent).replace('/', '.') + if identifier == 'feldfreund_devkit': + continue + + try: + module = importlib.import_module(identifier) + except Exception: + logging.exception('Failed to import %s', identifier) + sys.exit(1) + + doc_path = path.parent.with_suffix('.md') + found_something = False + for name in getattr(module, '__all__', dir(module)): + if name.startswith('_'): + continue + cls = getattr(module, name) + if isinstance(cls, ModuleType): + continue + if not inspect.isclass(cls): + continue + if not cls.__doc__: + continue + source_file = inspect.getfile(cls) + events = extract_events(source_file) + properties = extract_properties(cls) + instance_attrs = extract_instance_attributes(source_file) + if cls.__name__ != name: + cls_module = cls.__module__ + doc_identifier = f'{cls_module}.{cls.__name__}' + else: + doc_identifier = f'{identifier}.{name}' + properties = {k: v for k, v in properties.items() if v[1]} + instance_attrs = {k: v for k, v in instance_attrs.items() if v[1] and k not in events} + members = {**instance_attrs, **properties} + filters = list(events.keys()) + list(members.keys()) + with mkdocs_gen_files.open(Path('reference', doc_path), 'a') as fd: + print(f'::: {doc_identifier}', file=fd) + if filters: + print(' options:', file=fd) + print(' filters:', file=fd) + for filter_name in filters: + print(f' - "!{filter_name}"', file=fd) + if members: + print('### Attributes & Properties', file=fd) + print('Name | Type | Description', file=fd) + print('- | - | -', file=fd) + for member_name, (member_type, member_doc) in members.items(): + print(f'`{member_name}` | {member_type} | {member_doc}', file=fd) + print('', file=fd) + if events: + print('### Events', file=fd) + print('Name | Description', file=fd) + print('- | -', file=fd) + for event_name, event_doc in events.items(): + print(f'`{event_name}` | {event_doc}', file=fd) + print('', file=fd) + found_something = True + + if found_something: + nav[path.parent.parts[1:]] = doc_path.as_posix() + +with mkdocs_gen_files.open('reference/SUMMARY.md', 'w') as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..e3893d2 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,57 @@ +# Getting Started + +## Installation + +Clone the repository and install dependencies with [uv](https://docs.astral.sh/uv/): + +```bash +git clone https://github.com/zauberzeug/feldfreund_devkit.git +cd feldfreund_devkit +uv sync +``` + +## Running the Example + +The repository includes a minimal example in `main.py` that demonstrates: + +- Robot simulation with keyboard control +- Straight line navigation automation +- Real-time 3D visualization + +Start it with: + +```bash +uv run main.py +``` + +Open [http://localhost:8080](http://localhost:8080) in your browser. Hold **SHIFT** and use the **arrow keys** to steer the robot, or use the automation controls to run a straight line navigation. + +## Understanding the Example + +```python +--8<-- "main.py" +``` + +The `System` class extends `feldfreund_devkit.System` which initializes the robot hardware (or simulation) based on the configuration. Key components: + +- **config**: Loaded from `config/example.py` via `config_from_id('example')` +- **steerer**: Manual steering control +- **driver**: Path-following driver for automations +- **navigation**: `StraightLineNavigation` drives forward for a configurable distance +- **automator**: Manages automation lifecycle (play/pause/stop) + +## Configuration + +Robot configurations live in the `config/` directory. See `config/example.py`: + +```python +--8<-- "config/example.py" +``` + +In simulation mode (when no hardware is detected), mock implementations are used automatically. + +## Next Steps + +- Browse the **Module Reference** in the navigation for API documentation +- Check the [Tutorials](tutorials/tutorials.md) for hardware calibration guides +- See [Troubleshooting](troubleshooting.md) for common issues diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..5e98cee --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# About + +The **Feldfreund Dev Kit** is an open-source platform for autonomous outdoor robotics developed by [Zauberzeug GmbH](https://zauberzeug.com). +Built on [RoSys](https://rosys.io), an all-Python robot system framework with built-in simulation, hardware abstraction, and a real-time web interface via [NiceGUI](https://nicegui.io/). +Time-critical and safety-critical behavior runs on the microcontroller via [Lizard](https://lizard.dev), a domain-specific language for embedded hardware control. + +Our agricultural weeding robot [Feldfreund](https://zauberzeug.com/feldfreund) is based on this platform and is intended to advance organic and regenerative agriculture. + +**This documentation is currently work in progress while we expand this library.** + +Stay tuned and please [report any issues on Github](https://github.com/zauberzeug/feldfreund_devkit/issues). + +## Features + +- **Open Source** — modify and enhance the software to fit your specific needs. +- **Modular Design** — equip with tools from Zauberzeug, third-party solutions, or custom developments. +- **ROS2 Support** — a [ROS2 implementation](https://github.com/zauberzeug/feldfreund_devkit_ros) is available. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..d6ae0ef --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,27 @@ +:root { + --md-primary-fg-color: #6e93d6; + --md-default-fg-color: #3a3e42; + --md-accent-fg-color: #53b689; +} + +:root > * { + --md-code-hl-color: #d8e5fa; +} + +h2.doc-heading { + border-bottom: 1pt solid lightgray; +} + +img { + width: 70%; + display: block; + margin-left: auto; + margin-right: auto; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.25) 0 5px 10px; +} + +/* Hide "Defaults" section from mkdocstrings */ +details.defaults { + display: none; +} diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..3e14694 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,26 @@ +# Troubleshooting + +This section provides guidance for diagnosing and resolving issues during operation. +If your issue is not listed here, you can also check the [RoSys documentation](https://rosys.io/troubleshooting/). + +## Logs + +Check the logs for warnings or errors. Archived logs are stored in the `~/.rosys` directory. + +For more detail, enable debug-level logging on the [RoSys Logging Page](https://rosys.io/reference/rosys/analysis/#rosys.analysis.logging_page.LoggingPage) available at [/logging](http://localhost:8080/logging). + +## Permission denied directly after startup + +If you get the `[Errno 13] Permission denied` error message right after you started `main.py`, your system is probably blocking the default port 80. +Set a custom port via environment variable: + +```bash +PORT=8080 uv run ./main.py +``` + +Or add it to a `.env` file: + +``` +ROBOT_ID=my_robot +PORT=8080 +``` diff --git a/docs/tutorials/imu_calibration.md b/docs/tutorials/imu_calibration.md new file mode 100644 index 0000000..9748c7c --- /dev/null +++ b/docs/tutorials/imu_calibration.md @@ -0,0 +1,39 @@ +# Imu Calibration + +The IMU is not installed in its intended orientation, that is why we have to find the correct values. +The standard coordinate frame of our robots is a right-handed one where X is forward, Y is left and Z is upward. +The built-in IMUs orientation is X-left, Y-down and Z-backward, therefore a roll rotation of -90° and a yaw rotation of 90° is needed for a Robot Brain in its default configuration, where the socket connectors show backwards. +Older robots like U4 will need an additional yaw rotation of 90° afterwards, because their Robot Brains are built in sideways. + +Here is a short Python script to generate the needed configuration: + +```python +#!/usr/bin/env python3 +import numpy as np +from rosys.geometry import Rotation + +base_rotation = Rotation.from_euler(np.deg2rad(-90), 0, np.deg2rad(90)) +print(f'{base_rotation=}') + +roll = np.deg2rad(0.0) +pitch = np.deg2rad(0.0) +correction = Rotation.from_euler(roll, pitch, 0.0) +print(f'{correction=}') + +complete_correction = correction * base_rotation +print(f'{complete_correction=}') + +print('\nCode snippet for your robot\'s configuration:') +print(f'imu=Imu(offset_rotation=Rotation.from_euler({complete_correction.roll:.6f}, {complete_correction.pitch:.6f}, {complete_correction.yaw:.6f}))') +``` + +Steps: + +1. Find the correct base orientation of your IMU. + If your Feldfreund has the standard configuration, you can use this as a starting point: + `Imu(offset_rotation=Rotation.from_euler(-1.570796, -0.000000, 1.570796))` +2. Roll and pitch your robot manually to check if the configuration is correct and the axes are correctly rotated. +3. Place your robot on a level surface and check the IMU's current values on the [development page](http://192.168.42.2/dev) +4. Put the shown values for roll and pitch in the script above. + That will generate your final configuration values. + Note, that IMU values have some noise, so your configuration won't be perfect. diff --git a/docs/tutorials/odrive_calibration.md b/docs/tutorials/odrive_calibration.md new file mode 100644 index 0000000..a883f19 --- /dev/null +++ b/docs/tutorials/odrive_calibration.md @@ -0,0 +1,52 @@ +# ODrive Calibration + +## Flashing ODrive + +To flash an ODrive it has to be put into DFU Mode. +For this there is a switch on the black ODrive board. +This switch has to be put into the DFU setting. +Following this, the whole board needs to be power cycled (Including RdyP and the battery connection). +Then the ODrive needs to be connected to the Robot Brain using a micro-USB cable. + +Now, from the odrive directory, the following command can be run. +This will flash the STM chip with the newest firmware. + +```bash +./flash_odrive.sh fw-v5.6-feldfreund/ODriveFirmware.bin +``` + +## Calibrating ODrive + +To calibrate the ODrives, they have to be in the run mode(switch with DFU and run). +The normal Feldfreund Lizard script will interfere with the calibration script. +In order to prevent this the `calibration.liz` needs to be configured. +This can be done in the `/lizard` folder with the command: + +```bash +./configure.py /calibration.liz +``` + +If this is failing, make sure there are no other serial connections running to the ESP. + +Before starting the calibration make sure, that the tracks do not touch the ground and the ODrive is connected to the Robot Brain using the micro-USB cable. +Then for the right track run: + +```bash +python3 calibrate_two_motors_r.py +``` + +and for the left track: + +```bash +python3 calibrate_two_motors_l.py +``` + +If the calibration does not work calling it with `sudo` can help. +These scripts will set the motor parameters and start the calibration of the hall sensors. +When you are done, don't forget to reconfigure the ESP with your Feldfreund code. + +## Further debugging + +Further debugging can be done using the `odrivetool`. +While the ODrive is connected via USB the tool can be started from the command line. +For further documentation reference the [ODrive documentation](https://docs.odriverobotics.com/v/0.5.6/getting-started.html). diff --git a/docs/tutorials/tutorials.md b/docs/tutorials/tutorials.md new file mode 100644 index 0000000..60bb9cb --- /dev/null +++ b/docs/tutorials/tutorials.md @@ -0,0 +1,6 @@ +# Tutorials + +Welcome to the Feldfreund tutorials section! Here you'll find step-by-step guides to help you get the most out of your Feldfreund robot. + +- [IMU calibration](imu_calibration.md) - How to determine the correct rotation for the ImuConfiguration +- [ODrive calibration](odrive_calibration.md) - Let the ODrive motor controllers calibrate themselves diff --git a/feldfreund_devkit/api/online.py b/feldfreund_devkit/api/online.py index c57dba4..7fa315e 100644 --- a/feldfreund_devkit/api/online.py +++ b/feldfreund_devkit/api/online.py @@ -2,9 +2,13 @@ class Online: - """API endpoints for checking the robot's online status.""" + """API endpoints for checking the robot's online status. + + - `GET /api/online` → `{'online': True | False}` + """ def __init__(self) -> None: - @app.get('/api/online') - def connected(): - return {'online': True} + app.get('/api/online')(self.online) + + def online(self) -> dict[str, bool]: + return {'online': True} diff --git a/feldfreund_devkit/config/battery_configuration.py b/feldfreund_devkit/config/battery_configuration.py index accc7d7..f67c154 100644 --- a/feldfreund_devkit/config/battery_configuration.py +++ b/feldfreund_devkit/config/battery_configuration.py @@ -3,7 +3,7 @@ @dataclass(slots=True, kw_only=True) class BatteryControlConfiguration: - """Configuration for the battery control of the Field Friend robot. + """Configuration for the battery control of the Feldfreund robot. Defaults: name: 'battery_control' @@ -19,7 +19,7 @@ class BatteryControlConfiguration: @dataclass(slots=True, kw_only=True) class BmsConfiguration: - """Configuration for the bms of the Field Friend robot. + """Configuration for the bms of the Feldfreund robot. Defaults: name: 'bms' diff --git a/feldfreund_devkit/config/bumper_configuration.py b/feldfreund_devkit/config/bumper_configuration.py index 8f51cf0..9f075f6 100644 --- a/feldfreund_devkit/config/bumper_configuration.py +++ b/feldfreund_devkit/config/bumper_configuration.py @@ -3,7 +3,7 @@ @dataclass(slots=True, kw_only=True) class BumperConfiguration: - """Configuration for the bumper of the Field Friend robot. + """Configuration for the bumper of the Feldfreund robot. Defaults: name: 'bumper' diff --git a/feldfreund_devkit/config/camera_configuration.py b/feldfreund_devkit/config/camera_configuration.py index 11e2329..1480fda 100644 --- a/feldfreund_devkit/config/camera_configuration.py +++ b/feldfreund_devkit/config/camera_configuration.py @@ -6,7 +6,7 @@ @dataclass(kw_only=True) class CropConfiguration: - """Configuration for the cropping of the camera of the Field Friend robot.""" + """Configuration for the cropping of the camera of the Feldfreund robot.""" left: int right: int up: int @@ -31,14 +31,14 @@ class CircleSightPositions: @dataclass(kw_only=True) class CameraConfiguration: - """Configuration for the camera of the Field Friend robot. + """Configuration for the camera of the Feldfreund robot. - Attributes: - camera_type: default = 'CalibratableUsbCamera' - auto_exposure: default = True - rotation: default = 0 - fps: default = 10 - crop: default = None + Defaults: + camera_type: 'CalibratableUsbCamera' + auto_exposure: True + rotation: 0 + fps: 10 + crop: None """ width: int height: int @@ -50,7 +50,7 @@ class CameraConfiguration: @property def crop_rectangle(self) -> Rectangle | None: - """get a rectangle based on the crop values (left, right, up, down) of the config""" + """Get a rectangle based on the crop values (left, right, up, down) of the config""" if self.crop is None: return None new_width = self.width - (self.crop.left + self.crop.right) diff --git a/feldfreund_devkit/config/can_configuration.py b/feldfreund_devkit/config/can_configuration.py index 0503694..7443850 100644 --- a/feldfreund_devkit/config/can_configuration.py +++ b/feldfreund_devkit/config/can_configuration.py @@ -3,7 +3,7 @@ @dataclass(slots=True, kw_only=True) class CanConfiguration: - """Configuration for the can of the Field Friend robot. + """Configuration for the can of the Feldfreund robot. Defaults: name: 'can' diff --git a/feldfreund_devkit/config/estop_configuration.py b/feldfreund_devkit/config/estop_configuration.py index 2c3269a..8703006 100644 --- a/feldfreund_devkit/config/estop_configuration.py +++ b/feldfreund_devkit/config/estop_configuration.py @@ -3,7 +3,7 @@ @dataclass(slots=True, kw_only=True) class EstopConfiguration: - """Configuration for the estop of the Field Friend robot. + """Configuration for the estop of the Feldfreund robot. Defaults: name: 'estop' diff --git a/feldfreund_devkit/config/feldfreund_configuration.py b/feldfreund_devkit/config/feldfreund_configuration.py index 21cfaa2..af77369 100644 --- a/feldfreund_devkit/config/feldfreund_configuration.py +++ b/feldfreund_devkit/config/feldfreund_configuration.py @@ -18,6 +18,7 @@ @dataclass(kw_only=True) class FeldfreundConfiguration: + """Main configuration for a Feldfreund robot combining all module configurations.""" robot_id: str battery_control: BatteryControlConfiguration = field(default_factory=BatteryControlConfiguration) bluetooth: BluetoothConfiguration = field(default_factory=BluetoothConfiguration) diff --git a/feldfreund_devkit/config/gnss_configuration.py b/feldfreund_devkit/config/gnss_configuration.py index e6d36ae..6e22f12 100644 --- a/feldfreund_devkit/config/gnss_configuration.py +++ b/feldfreund_devkit/config/gnss_configuration.py @@ -5,7 +5,7 @@ @dataclass(slots=True, kw_only=True) class GnssConfiguration: - """Configuration for the GNSS of the Field Friend robot. + """Configuration for the GNSS of the Feldfreund robot. X, Y, Z are the position of the main GNSS antenna. The yaw is the direction to the auxiliary antenna. diff --git a/feldfreund_devkit/config/imu_configuration.py b/feldfreund_devkit/config/imu_configuration.py index 02a531b..02cbe27 100644 --- a/feldfreund_devkit/config/imu_configuration.py +++ b/feldfreund_devkit/config/imu_configuration.py @@ -5,7 +5,7 @@ @dataclass(slots=True, kw_only=True) class ImuConfiguration: - """Configuration for the IMU of the Field Friend robot. + """Configuration for the IMU of the Feldfreund robot. Defaults: name: 'imu' diff --git a/feldfreund_devkit/config/robot_brain_configuration.py b/feldfreund_devkit/config/robot_brain_configuration.py index 1eb60fa..195b9f7 100644 --- a/feldfreund_devkit/config/robot_brain_configuration.py +++ b/feldfreund_devkit/config/robot_brain_configuration.py @@ -3,7 +3,7 @@ @dataclass(kw_only=True) class RobotBrainConfiguration: - """Configuration for the robot brain of the Field Friend robot. + """Configuration for the robot brain of the Feldfreund robot. Defaults: enable_esp_on_startup: False diff --git a/feldfreund_devkit/feldfreund.py b/feldfreund_devkit/feldfreund.py index 7a743fa..406b134 100644 --- a/feldfreund_devkit/feldfreund.py +++ b/feldfreund_devkit/feldfreund.py @@ -53,6 +53,8 @@ class Feldfreund(Robot): + """Base class representing a Feldfreund robot with all its hardware modules.""" + def __init__(self, config: FeldfreundConfiguration, *, bms: Bms, bumper: Bumper | None, @@ -89,6 +91,8 @@ async def stop(self) -> None: class FeldfreundHardware(Feldfreund, RobotHardware): + """Hardware implementation of a Feldfreund robot with real hardware modules.""" + def __init__(self, config: FeldfreundConfiguration, **kwargs) -> None: communication = SerialCommunication() robot_brain = RobotBrain(communication, @@ -208,6 +212,8 @@ def _setup_flashlight(self, config: FlashlightConfiguration | FlashlightMosfetCo class FeldfreundSimulation(Feldfreund, RobotSimulation): + """Simulated Feldfreund robot for testing and development.""" + def __init__(self, config: FeldfreundConfiguration, *, use_acceleration: bool = False, **kwargs) -> None: wheels = TracksSimulation(config.wheels.width) if use_acceleration \ else WheelsSimulation(config.wheels.width) diff --git a/feldfreund_devkit/hardware/flashlight.py b/feldfreund_devkit/hardware/flashlight.py index fcc08d2..3577e3f 100644 --- a/feldfreund_devkit/hardware/flashlight.py +++ b/feldfreund_devkit/hardware/flashlight.py @@ -9,6 +9,8 @@ class Flashlight(rosys.hardware.Module, abc.ABC): + """Base class for flashlight modules with on/off and duty cycle control.""" + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self._duty_cycle: float = 1.0 @@ -52,6 +54,8 @@ def developer_ui(self) -> None: class FlashlightHardware(Flashlight, rosys.hardware.ModuleHardware, SafetyMixin): + """Flashlight hardware implementation using PWM outputs.""" + def __init__(self, config: FlashlightConfiguration, robot_brain: rosys.hardware.RobotBrain, *, expander: rosys.hardware.ExpanderHardware | None) -> None: @@ -115,6 +119,8 @@ async def set_duty_cycle(self, duty_cycle: float) -> None: class FlashlightHardwareMosfet(Flashlight, rosys.hardware.ModuleHardware, SafetyMixin): + """Flashlight hardware implementation using a MOSFET switch.""" + UPDATE_INTERVAL = 5.0 def __init__(self, config: FlashlightMosfetConfiguration, @@ -164,4 +170,4 @@ def developer_ui(self) -> None: class FlashlightSimulation(Flashlight, rosys.hardware.ModuleSimulation): - ... + """Simulated flashlight for testing.""" diff --git a/feldfreund_devkit/hardware/safety.py b/feldfreund_devkit/hardware/safety.py index 21e8a00..9c26664 100644 --- a/feldfreund_devkit/hardware/safety.py +++ b/feldfreund_devkit/hardware/safety.py @@ -6,6 +6,11 @@ class SafetyMixin(ABC): + """Mixin for modules that integrate with the safety system. + + Implement `enable_code` and `disable_code` to return Lizard code snippets + that get injected into the safety module's `enable()` and `disable()` functions. + """ @property @abstractmethod @@ -19,7 +24,11 @@ def disable_code(self) -> str: class Safety(ABC): - """The safety module is a simple example for a representation of real or simulated robot hardware.""" + """Coordinates safety behavior across wheels, estop, bumper, and registered modules. + + Modules implementing `SafetyMixin` can be registered via `add_module()` to have their + enable/disable code automatically called when the safety state changes. + """ def __init__(self, *, wheels: rosys.hardware.Wheels, @@ -32,11 +41,21 @@ def __init__(self, *, self.modules = modules or [] def add_module(self, module: SafetyMixin) -> None: + """Register a module to be enabled/disabled with the safety system.""" self.modules.append(module) class SafetyHardware(Safety, rosys.hardware.ModuleHardware): - """This module implements safety hardware.""" + """Generates Lizard code for hardware safety with automatic enable/disable behavior. + + The generated code creates `enable()` and `disable()` functions that control + wheels and all registered `SafetyMixin` modules. State transitions: + + - E-stop or bumper triggered → `disable()` called + - E-stop and bumper released while disabled → `enable()` called + - No messages for 1s → wheels stop + - No messages for 20s → full disable (watchdog) + """ def __init__(self, robot_brain: rosys.hardware.RobotBrain, **kwargs) -> None: Safety.__init__(self, **kwargs) @@ -109,4 +128,4 @@ async def estop_released_safety_notifications(self, name: str) -> None: class SafetySimulation(Safety, rosys.hardware.ModuleSimulation): - ... + """Simulated safety module for testing.""" diff --git a/feldfreund_devkit/hardware/teltonika_router.py b/feldfreund_devkit/hardware/teltonika_router.py index e7b0749..36074d4 100644 --- a/feldfreund_devkit/hardware/teltonika_router.py +++ b/feldfreund_devkit/hardware/teltonika_router.py @@ -8,6 +8,7 @@ class ConnectionStatus(Enum): + """Connection status of the Teltonika router.""" ETHER = 'ether' WIFI = 'wifi' MOBILE = 'mobile' diff --git a/feldfreund_devkit/hardware/tracks.py b/feldfreund_devkit/hardware/tracks.py index 888753e..2fdc84a 100644 --- a/feldfreund_devkit/hardware/tracks.py +++ b/feldfreund_devkit/hardware/tracks.py @@ -8,7 +8,7 @@ class TracksHardware(Wheels, ModuleHardware): - """Expands the RoSys wheels hardware to control the field friend's tracked wheels with dual motors.""" + """Expands the RoSys wheels hardware to control the Feldfreund's tracked wheels with dual motors.""" MAX_VALID_LINEAR_VELOCITY = 3.0 MAX_VALID_ANGULAR_VELOCITY = 3.5 ERROR_FLAG_VERSION = 6 @@ -137,13 +137,9 @@ def _ui() -> None: class TracksSimulation(WheelsSimulation): - def __init__(self, width: float = 0.5, *, linear_acceleration: float = 2.0, linear_deceleration: float = 0.5) -> None: - """Simulate differential drive wheels with acceleration and deceleration handling. + """Simulated tracks with acceleration and deceleration handling.""" - :param width: The distance between the wheels in meters. - :param linear_acceleration: The maximum linear acceleration rate in m/s². - :param linear_deceleration: The maximum linear deceleration rate in m/s². - """ + def __init__(self, width: float = 0.5, *, linear_acceleration: float = 2.0, linear_deceleration: float = 0.5) -> None: super().__init__(width) self.linear_acceleration: float = linear_acceleration @@ -154,12 +150,12 @@ def __init__(self, width: float = 0.5, *, linear_acceleration: float = 2.0, line @property def angular_acceleration(self) -> float: - """Calculate angular acceleration from linear acceleration using differential drive kinematics.""" + """Calculated angular acceleration from linear acceleration using differential drive kinematics.""" return 2 * self.linear_acceleration / self.width @property def angular_deceleration(self) -> float: - """Calculate angular deceleration from linear deceleration using differential drive kinematics.""" + """Calculated angular deceleration from linear deceleration using differential drive kinematics.""" return 2 * self.linear_deceleration / self.width async def drive(self, linear: float, angular: float) -> None: diff --git a/feldfreund_devkit/implement.py b/feldfreund_devkit/implement.py index fb51fd0..a08e7d4 100644 --- a/feldfreund_devkit/implement.py +++ b/feldfreund_devkit/implement.py @@ -9,10 +9,11 @@ class ImplementException(Exception): - pass + """Raised when an implement operation fails.""" class Implement(rosys.persistence.Persistable): + """Base class for robot implements like weeding tools or cameras.""" def __init__(self, config: ImplementConfiguration) -> None: super().__init__() @@ -87,6 +88,8 @@ def developer_ui(self) -> None: class ImplementDummy(Implement): + """A no-op implement for testing or when no implement is attached.""" + def __init__(self) -> None: super().__init__(ImplementConfiguration(lizard_name='None', display_name='None', work_radius=0.0)) diff --git a/feldfreund_devkit/interface/components/confirm_dialog.py b/feldfreund_devkit/interface/components/confirm_dialog.py index 9d3d2fe..c526510 100644 --- a/feldfreund_devkit/interface/components/confirm_dialog.py +++ b/feldfreund_devkit/interface/components/confirm_dialog.py @@ -2,13 +2,9 @@ class ConfirmDialog(ui.dialog): - def __init__(self, text: str = 'Are you sure?', *, delay: float = 3.0) -> None: - """ - A dialog that asks for confirmation. + """A dialog that asks for confirmation with a delayed yes button.""" - :param text: The text to display in the dialog. - :param delay: The delay in seconds before the yes button is enabled. - """ + def __init__(self, text: str = 'Are you sure?', *, delay: float = 3.0) -> None: super().__init__() self.delay = delay with self, ui.card(): diff --git a/feldfreund_devkit/interface/components/header_bar.py b/feldfreund_devkit/interface/components/header_bar.py index 71007ac..6a323da 100644 --- a/feldfreund_devkit/interface/components/header_bar.py +++ b/feldfreund_devkit/interface/components/header_bar.py @@ -5,6 +5,8 @@ class HeaderBar: + """Navigation header with logo, page links, and status indicators.""" + def __init__(self, pages: dict[str, str] | None = None, *, estop: EStop | None = None, bms: Bms | None = None, diff --git a/feldfreund_devkit/interface/components/log_monitor.py b/feldfreund_devkit/interface/components/log_monitor.py index 308e571..99b44d3 100644 --- a/feldfreund_devkit/interface/components/log_monitor.py +++ b/feldfreund_devkit/interface/components/log_monitor.py @@ -7,6 +7,8 @@ class LogMonitor(rosys.persistence.Persistable): + """Persisted log display for notifications with timestamps.""" + MAX_LINES = 100 def __init__(self, *, max_lines: int = MAX_LINES) -> None: diff --git a/feldfreund_devkit/interface/components/status_bulb.py b/feldfreund_devkit/interface/components/status_bulb.py index 2a915fc..ec9c524 100644 --- a/feldfreund_devkit/interface/components/status_bulb.py +++ b/feldfreund_devkit/interface/components/status_bulb.py @@ -2,6 +2,8 @@ class StatusBulb(ValueElement): + """A circular LED-style indicator that changes color based on value.""" + def __init__(self, value: bool = False) -> None: super().__init__(value=value, on_value_change=self.on_change, tag='span') self.style('height: 15px; width: 15px; margin: auto; border-radius: 50%') diff --git a/feldfreund_devkit/navigation/drive_segment.py b/feldfreund_devkit/navigation/drive_segment.py index 6285585..02670da 100644 --- a/feldfreund_devkit/navigation/drive_segment.py +++ b/feldfreund_devkit/navigation/drive_segment.py @@ -7,6 +7,7 @@ @dataclass(slots=True, kw_only=True) class DriveSegment(PathSegment): + """A path segment with implement usage and stop behavior configuration.""" # TODO: move methods to rosys.driving.PathSegment use_implement: bool = False stop_at_end: bool = True diff --git a/feldfreund_devkit/navigation/straight_line_navigation.py b/feldfreund_devkit/navigation/straight_line_navigation.py index e132932..9c7e85f 100644 --- a/feldfreund_devkit/navigation/straight_line_navigation.py +++ b/feldfreund_devkit/navigation/straight_line_navigation.py @@ -7,6 +7,7 @@ class StraightLineNavigation(WaypointNavigation): + """Navigation that drives a straight line for a given length.""" LENGTH: float = 2.0 def __init__(self, **kwargs) -> None: diff --git a/feldfreund_devkit/navigation/waypoint_navigation.py b/feldfreund_devkit/navigation/waypoint_navigation.py index 6a91c7f..9394dae 100644 --- a/feldfreund_devkit/navigation/waypoint_navigation.py +++ b/feldfreund_devkit/navigation/waypoint_navigation.py @@ -16,6 +16,8 @@ class WaypointNavigation(rosys.persistence.Persistable): + """Base class for all waypoint based navigation types.""" + LINEAR_SPEED_LIMIT: float = 0.13 def __init__(self, *, implement: Implement, driver: Driver, pose_provider: PoseProvider, name: str = 'Waypoint Navigation') -> None: @@ -29,7 +31,7 @@ def __init__(self, *, implement: Implement, driver: Driver, pose_provider: PoseP self.linear_speed_limit = self.LINEAR_SPEED_LIMIT self.PATH_GENERATED = Event[list[DriveSegment]]() - """a new path has been generated (argument: ``list[DriveSegment]``)""" + """a new path has been generated(argument: ``list[DriveSegment]``)""" self.SEGMENT_STARTED = Event[DriveSegment]() """a waypoint has been reached""" @@ -42,17 +44,19 @@ def __init__(self, *, implement: Implement, driver: Driver, pose_provider: PoseP @property def path(self) -> list[DriveSegment]: + """Returns the whole planned path.""" return self._upcoming_path @property def current_segment(self) -> DriveSegment | None: + """Returns the current segment to drive along or None if there are no waypoints left.""" if not self._upcoming_path: return None return self._upcoming_path[0] @property def has_waypoints(self) -> bool: - """Returns True as long as there are waypoints to drive to""" + """Returns True as long as there are waypoints to drive to.""" return self.current_segment is not None @track @@ -156,7 +160,7 @@ async def _block_until_implement_has_target(self) -> Point: def _remove_segments_behind_robot(self, path_segments: list[DriveSegment], *, completed_percentage: float = 0.99) -> list[DriveSegment]: - """Create new path (list of segments) starting at the closest segment to the current pose""" + """Create new path(list of segments) starting at the closest segment to the current pose""" current_pose = self.pose_provider.pose start_index = 0 for i, segment in enumerate(path_segments): diff --git a/feldfreund_devkit/robot_locator.py b/feldfreund_devkit/robot_locator.py index 24ed006..74ffe84 100644 --- a/feldfreund_devkit/robot_locator.py +++ b/feldfreund_devkit/robot_locator.py @@ -12,6 +12,8 @@ class RobotLocator(rosys.persistence.Persistable): + """Extended Kalman filter for robot pose estimation using odometry, GNSS, and IMU.""" + R_ODOM_LINEAR = 0.1 R_ODOM_ANGULAR = 0.097 R_IMU_ANGULAR = 0.01 @@ -160,7 +162,7 @@ def _handle_gnss_measurement(self, gnss_measurement: GnssMeasurement) -> None: return if not np.isfinite(gnss_measurement.heading_std_dev): # normally we would only handle the position if no heading is available, - # but the field friend needs the rtk accuracy to function properly + # but the Feldfreund needs the rtk accuracy to function properly return pose, r_xy, r_theta = self._get_local_pose_and_uncertainty(gnss_measurement) if self._auto_tilt_correction and isinstance(self._imu, Imu) and not self._ignore_imu and self._imu.last_measurement is not None: diff --git a/main.py b/main.py index 0780a77..6d319c8 100755 --- a/main.py +++ b/main.py @@ -1,21 +1,43 @@ #! /usr/bin/env python +import rosys from nicegui import app, ui -from rosys.analysis.logging_page import LoggingPage +from rosys.automation import Automator, automation_controls +from rosys.driving import Driver, Steerer, keyboard_control, robot_object +import feldfreund_devkit +from feldfreund_devkit.config import FeldfreundConfiguration, config_from_id +from feldfreund_devkit.implement import ImplementDummy +from feldfreund_devkit.navigation import StraightLineNavigation -def startup() -> None: - @ui.page('/') - def home_page() -> None: - ui.label('Hello to your new project: Feldfreund_devkit!').classes('text-4xl absolute-center') +class System(feldfreund_devkit.System): + def __init__(self, config: FeldfreundConfiguration) -> None: + super().__init__(config) + self.steerer = Steerer(self.feldfreund.wheels, speed_scaling=0.25) + self.driver = Driver(self.feldfreund.wheels, self.odometer, parameters=self.config.driver) + self.shape = rosys.geometry.Prism.default_robot_shape() + self.navigation = StraightLineNavigation(implement=ImplementDummy(), + driver=self.driver, + pose_provider=self.odometer) + self.automator = Automator(self.steerer, on_interrupt=self.feldfreund.stop, notify=False) + self.automator.default_automation = self.navigation.start - logging_groups = ['Feldfreund_devkit', 'rosys', 'nicegui'] - LoggingPage(logging_groups) +def startup() -> None: + config = config_from_id('example') + system = System(config).persistent() -@app.get('/status') -def status() -> dict[str, str]: - return {'status': 'ok'} + @ui.page('/') + def ui_content() -> None: + keyboard_control(system.steerer) + with ui.scene(): + robot_object(system.shape, system.odometer) + with ui.card(): + ui.label('hold SHIFT to steer with the keyboard arrow keys or use the automation controls') + with ui.row(): + system.navigation.settings_ui() + with ui.row(): + automation_controls(system.automator) app.on_startup(startup) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..bce2611 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,59 @@ +site_name: Feldfreund Dev Kit Documentation +site_url: https://docs.feldfreund.de/ +nav: + - index.md + - getting_started.md + - Tutorials: + - tutorials/tutorials.md + - tutorials/imu_calibration.md + - tutorials/odrive_calibration.md + - Module Reference: reference/ + - troubleshooting.md +repo_url: https://github.com/zauberzeug/feldfreund_devkit +edit_uri: edit/main/docs/ +theme: + name: material + font: + text: Source Sans Pro + features: + - content.code.annotate +extra_css: + - stylesheets/extra.css +markdown_extensions: + - toc: + permalink: True + - admonition + - def_list + - mdx_include: + base_path: docs + - pymdownx.highlight + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets + - attr_list + - footnotes +plugins: + - search + - gen-files: + scripts: + - docs/generate_reference.py + - literate-nav: + nav_file: SUMMARY.md + - section-index + - mkdocstrings: + default_handler: python + handlers: + python: + options: + show_root_heading: true + show_root_full_path: false + show_source: false + show_signature_annotations: true + merge_init_into_class: true + separate_signature: true + docstring_section_style: table + filters: + - "!^_[^_]" +watch: + - feldfreund_devkit + - docs diff --git a/odrive/README.md b/odrive/README.md deleted file mode 100644 index f6a36b5..0000000 --- a/odrive/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# ODrive Calibration - -## Flashing ODrive - -To flash an ODrive it has to be put into DFU Mode. For this there is a switch on the black ODrive board. This switch has to be put into the DFU setting. Following this, the whole board needs to be power cycled (Including RdyP and the battery connection). -Then the ODrive needs to be connected to the Robot Brain using a micro-USB cable. Now the command - -```bash -./flash_odrive.sh fw-v5.6-feldfreund/ODriveFirmware.bin -``` - -can be run. This will flash the STM chip with the newest firmware. - -## Calibrating ODirve - -To calibrate the ODrives, they have to be in the run mode(switch with DFU and run). The normal Feldfreund Lizard script will interfere with the calibration script. In order to prevent this the `calibration.liz` needs to be configured. This can be done in the `/lizard` folder with the command: - -```bash -./configure.py /calibration.liz -``` - -If this is failing, make sure there are no other serial connections running to the ESP. - -Before starting the calibration make sure, that the tracks do not touch the ground and the ODrive is connected to the Robot Brain using the micro-USB cable. Then for the right track run: - -```bash -python3 calibrate_two_motors_r.py -``` - -and for the left track: - -```bash -python3 calibrate_two_motors_l.py -``` - -If the calibration does not work calling it with `sudo` can help. - -These scripts will set the motor parameters and start the calibration of the hall sensors. - -When you are done, don't forget to reconfigure the ESP with your Feldfreund code. - -## Further debugging - -Further debugging can be done using the `odrivetool`. While the ODrive is connected via USB the tool can be started from the command line. For further documentation reference the [ODrive documentation](https://docs.odriverobotics.com/v/0.5.6/getting-started.html). diff --git a/pyproject.toml b/pyproject.toml index 1e367fb..8064362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" requires-python = ">=3.11, <3.14" dependencies = [ "nicegui (>= 3.2.0, <4.0.0)", - "rosys @ git+https://github.com/zauberzeug/rosys.git@5298661def1a8dbe4b654c56a5334919d32cd9f3", + "rosys @ git+https://github.com/zauberzeug/rosys.git@bf941eb56ba2cc3254fa3b5c6c35cd54f173277a", "httpx >=0.25.0", "coloredlogs", "httpcore", @@ -23,6 +23,7 @@ dev = [ "ruff (>=0.13.2, <1.0.0)", "autopep8 (>=2.3.2, <3.0.0)", "poetry-dynamic-versioning (>=1.8.0, <2.0.0)", + "livesync (>=0.3.4, <1.0.0)", ] test = [ "pytest (>=8.3.4, <9.0.0)", @@ -31,6 +32,21 @@ test = [ types = [ "types-psutil>=7.1.3.20251211", ] +docs = [ + "black", + "click (<8.3.0)", + "mdx-footnotes2", + "mdx-include", + "mkdocs (>=1.6.1)", + "mkdocs-gen-files", + "mkdocs-literate-nav", + "mkdocs-macros-plugin", + "mkdocs-material", + "mkdocs-pymdownx-material-extras", + "mkdocs-section-index", + "mkdocstrings-python", + "toml", +] [build-system] requires = [ diff --git a/uv.lock b/uv.lock index c2c10c1..189498a 100644 --- a/uv.lock +++ b/uv.lock @@ -173,6 +173,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + [[package]] name = "bidict" version = "0.23.1" @@ -182,6 +204,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, ] +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + [[package]] name = "cairocffi" version = "1.7.1" @@ -335,14 +389,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] @@ -448,6 +502,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "cyclic" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167, upload-time = "2018-09-26T16:47:07.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547, upload-time = "2018-09-26T16:47:05.609Z" }, +] + [[package]] name = "dataclasses-json" version = "0.5.14" @@ -539,6 +602,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "autopep8" }, + { name = "livesync" }, { name = "mypy" }, { name = "poetry-dynamic-versioning" }, { name = "pre-commit" }, @@ -546,6 +610,21 @@ dev = [ { name = "ruff" }, { name = "types-psutil" }, ] +docs = [ + { name = "black" }, + { name = "click" }, + { name = "mdx-footnotes2" }, + { name = "mdx-include" }, + { name = "mkdocs" }, + { name = "mkdocs-gen-files" }, + { name = "mkdocs-literate-nav" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-pymdownx-material-extras" }, + { name = "mkdocs-section-index" }, + { name = "mkdocstrings-python" }, + { name = "toml" }, +] test = [ { name = "pytest" }, { name = "pytest-asyncio" }, @@ -561,19 +640,35 @@ requires-dist = [ { name = "httpx", specifier = ">=0.25.0" }, { name = "nicegui", specifier = ">=3.2.0,<4.0.0" }, { name = "psutil" }, - { name = "rosys", git = "https://github.com/zauberzeug/rosys.git?rev=5298661def1a8dbe4b654c56a5334919d32cd9f3" }, + { name = "rosys", git = "https://github.com/zauberzeug/rosys.git?rev=bf941eb56ba2cc3254fa3b5c6c35cd54f173277a" }, ] [package.metadata.requires-dev] dev = [ { name = "autopep8", specifier = ">=2.3.2,<3.0.0" }, + { name = "livesync", specifier = ">=0.3.4,<1.0.0" }, { name = "mypy", specifier = ">=1.18.2,<2.0.0" }, - { name = "poetry-dynamic-versioning", specifier = ">=1.8.0,<2" }, + { name = "poetry-dynamic-versioning", specifier = ">=1.8.0,<2.0.0" }, { name = "pre-commit", specifier = ">=4.3.0,<5.0.0" }, { name = "pylint", specifier = ">=3.3.3,<4.0.0" }, { name = "ruff", specifier = ">=0.13.2,<1.0.0" }, { name = "types-psutil", specifier = ">=7.1.3.20251211" }, ] +docs = [ + { name = "black" }, + { name = "click", specifier = "<8.3.0" }, + { name = "mdx-footnotes2" }, + { name = "mdx-include" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-gen-files" }, + { name = "mkdocs-literate-nav" }, + { name = "mkdocs-macros-plugin" }, + { name = "mkdocs-material" }, + { name = "mkdocs-pymdownx-material-extras" }, + { name = "mkdocs-section-index" }, + { name = "mkdocstrings-python" }, + { name = "toml" }, +] test = [ { name = "pytest", specifier = ">=8.3.4,<9.0.0" }, { name = "pytest-asyncio", specifier = ">=1.2.0,<2.0.0" }, @@ -695,6 +790,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -704,6 +823,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hjson" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/e5/0b56d723a76ca67abadbf7fb71609fb0ea7e6926e94fcca6c65a85b36a0e/hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75", size = 40541, upload-time = "2022-08-13T02:53:01.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018, upload-time = "2022-08-13T02:52:59.899Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1010,6 +1138,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/9c/ab8a94c30c082caca87bc0db78efe91372e45d35a700ef07ffe78ed10cda/line_profiler-4.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:727e970d358616a1a33d51d696efec932a5ef7730785df62658bd7e74aa58951", size = 128232, upload-time = "2024-12-03T17:11:51.741Z" }, ] +[[package]] +name = "livesync" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d6/fed2ee6f4b1ec828c8c5c4775d3d4384599dc4bc349416a67d3ba2ef312d/LiveSync-0.3.4.tar.gz", hash = "sha256:1748c4b2ce7820a6307b0e984a2d7844d74fa8f4a0c4d8f0e6b9a05a3aea34bb", size = 7223, upload-time = "2024-07-04T16:14:30.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/fe/c5bfc498121735833ad21bb1b65ad52daf913c9a18b71d672c71c38074c0/LiveSync-0.3.4-py3-none-any.whl", hash = "sha256:aa6d529fc281a82b6640171f566b6c20b3d29edc3fb51cf5e1bbd5f2b13b448b", size = 9111, upload-time = "2024-07-04T16:14:28.852Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + [[package]] name = "markdown2" version = "2.5.4" @@ -1142,6 +1292,225 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "mdx-footnotes2" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/0b/bb28aef565b22becb5d0354232baae3a7b9d8d6af5e0f95b30a260539f98/mdx-footnotes2-0.3.0.tar.gz", hash = "sha256:a56210594f7aee3ee1956dc64acad4bb5bf317278d111323e638c89292b6927e", size = 6946, upload-time = "2021-04-09T07:55:13.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/35/fa5c6a0f9b67e0275ba69ea6b33b25e2140c1a916fc6b66059165dce1c7a/mdx_footnotes2-0.3.0-py3-none-any.whl", hash = "sha256:0d7179870c645027187d5ca4c24deba611912fe9d3e619ecd4804450a944d077", size = 7149, upload-time = "2021-04-09T07:55:12.439Z" }, +] + +[[package]] +name = "mdx-include" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclic" }, + { name = "markdown" }, + { name = "rcslice" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051, upload-time = "2022-07-26T05:46:14.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591, upload-time = "2022-07-26T05:46:11.518Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, +] + +[[package]] +name = "mkdocs-gen-files" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/35/f26349f7fa18414eb2e25d75a6fa9c7e3186c36e1d227c0b2d785a7bd5c4/mkdocs_gen_files-0.6.0.tar.gz", hash = "sha256:52022dc14dcc0451e05e54a8f5d5e7760351b6701eff816d1e9739577ec5635e", size = 8642, upload-time = "2025-11-23T12:13:22.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/ec/72417415563c60ae01b36f0d497f1f4c803972f447ef4fb7f7746d6e07db/mkdocs_gen_files-0.6.0-py3-none-any.whl", hash = "sha256:815af15f3e2dbfda379629c1b95c02c8e6f232edf2a901186ea3b204ab1135b2", size = 8182, upload-time = "2025-11-23T12:13:20.756Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-literate-nav" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/5f/99aa379b305cd1c2084d42db3d26f6de0ea9bf2cc1d10ed17f61aff35b9a/mkdocs_literate_nav-0.6.2.tar.gz", hash = "sha256:760e1708aa4be86af81a2b56e82c739d5a8388a0eab1517ecfd8e5aa40810a75", size = 17419, upload-time = "2025-03-18T21:53:09.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/84/b5b14d2745e4dd1a90115186284e9ee1b4d0863104011ab46abb7355a1c3/mkdocs_literate_nav-0.6.2-py3-none-any.whl", hash = "sha256:0a6489a26ec7598477b56fa112056a5e3a6c15729f0214bea8a4dbc55bd5f630", size = 13261, upload-time = "2025-03-18T21:53:08.1Z" }, +] + +[[package]] +name = "mkdocs-macros-plugin" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hjson" }, + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "super-collections" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/15/e6a44839841ebc9c5872fa0e6fad1c3757424e4fe026093b68e9f386d136/mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f", size = 37730, upload-time = "2025-11-13T08:08:55.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/62/9fffba5bb9ed3d31a932ad35038ba9483d59850256ee0fea7f1187173983/mkdocs_macros_plugin-1.5.0-py3-none-any.whl", hash = "sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f", size = 44626, upload-time = "2025-11-13T08:08:53.878Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-pymdownx-material-extras" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs-material" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f6/5199d1e251e15b3c554f46b5796d02870e129f524dc2f117e828c5444674/mkdocs_pymdownx_material_extras-2.8.tar.gz", hash = "sha256:7b22bb119cd9592f98d6c6d4d269506d9a68d7038355c71525aadc88169ee9fe", size = 26512, upload-time = "2025-03-16T14:24:50.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/78/0059cb24b62a3dd68293daea38acb2cb75415d5735a62d6d60f752cd00ed/mkdocs_pymdownx_material_extras-2.8-py3-none-any.whl", hash = "sha256:81b68789420c51b9b15514180d0f3ab7136d56ee512c830c998d2edb77ca3d77", size = 28846, upload-time = "2025-03-16T14:24:49.235Z" }, +] + +[[package]] +name = "mkdocs-section-index" +version = "0.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/40/4aa9d3cfa2ac6528b91048847a35f005b97ec293204c02b179762a85b7f2/mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8", size = 14446, upload-time = "2025-04-05T20:56:45.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/53/76c109e6f822a6d19befb0450c87330b9a6ce52353de6a9dda7892060a1f/mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776", size = 8796, upload-time = "2025-04-05T20:56:43.975Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocs-get-deps" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/48/d134ffefd61349ac96161078d836c7e0c15062e01104327c9a5b23398a0f/mkdocstrings-0.28.3.tar.gz", hash = "sha256:c753516b1b6cee12d00bf9c28255e22c0d71f34c721ca668971fce885d846e0f", size = 104109, upload-time = "2025-03-08T21:43:21.088Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5c/205e4991fad1fbfe78b0d1fcfcf85f55556bcc93a5d6d94c7935e8463b87/mkdocstrings-0.28.3-py3-none-any.whl", hash = "sha256:df5351ffd10477aa3c2ff5cdf17544b936477195436923660274d084a5c1359c", size = 35177, upload-time = "2025-03-08T21:43:19.355Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -1276,7 +1645,7 @@ wheels = [ [[package]] name = "nicegui" -version = "3.5.0" +version = "3.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -1301,9 +1670,9 @@ dependencies = [ { name = "uvicorn", extra = ["standard"] }, { name = "watchfiles" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/09/ace5c379c1bcd4e454fe01e32f14e4649456b5f60efb5f73c5737e8cd208/nicegui-3.5.0.tar.gz", hash = "sha256:de802505d76ac3235088b19a32dba0f15cf51caf0d5b0fa6e196bfa0bc247af6", size = 21177233, upload-time = "2026-01-08T09:18:26.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/82d966c6c84fc95847ceb9eb439eaf7087eb07bdc30c19903d6390de0901/nicegui-3.6.1.tar.gz", hash = "sha256:40f56f18f23023d03a15c217d62c0bf801e0c874745c7b5995c890ef01af3dfb", size = 21202754, upload-time = "2026-01-21T16:41:06.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/24/ff7ebfc2da4336687ba86813f96791dbd42e6d23a15301105d8353cc4652/nicegui-3.5.0-py3-none-any.whl", hash = "sha256:27d1659bbeeb543c96a9dad0c4da586896becbfec117bf8ed122a6e5b696340c", size = 21839035, upload-time = "2026-01-08T09:18:23.953Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/19539da813ab65c9d9fa08236fa891cddb5dfc55031fefaf475f50cd0aa6/nicegui-3.6.1-py3-none-any.whl", hash = "sha256:41c9ae11422ed3ac76f3d054dac600449e437a230c2a692d62cde847337f1548", size = 21865440, upload-time = "2026-01-21T16:41:02.926Z" }, ] [[package]] @@ -1451,13 +1820,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, ] [[package]] @@ -1795,6 +2173,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/fd/f4a4d9b8ea16d59a3d6a76469983d5a88cfeb58959e09c08402baae2da3a/pyloot-0.1.0-py3-none-any.whl", hash = "sha256:7173439ad6c4adbd7019af93fae79efc96b2a7ccaff5363238246c588cd05f12", size = 189880, upload-time = "2022-12-05T20:28:14.541Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + [[package]] name = "pyparsing" version = "3.2.5" @@ -1923,6 +2314,30 @@ asyncio-client = [ { name = "aiohttp" }, ] +[[package]] +name = "pytokens" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" }, + { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" }, + { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" }, +] + [[package]] name = "pyturbojpeg" version = "1.8.2" @@ -1978,6 +2393,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "rcslice" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414, upload-time = "2018-09-27T12:44:06.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180, upload-time = "2018-09-27T12:44:05.197Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1995,8 +2431,8 @@ wheels = [ [[package]] name = "rosys" -version = "0.32.0.post2.dev0+5298661d" -source = { git = "https://github.com/zauberzeug/rosys.git?rev=5298661def1a8dbe4b654c56a5334919d32cd9f3#5298661def1a8dbe4b654c56a5334919d32cd9f3" } +version = "0.32.0.post5.dev0+bf941eb5" +source = { git = "https://github.com/zauberzeug/rosys.git?rev=bf941eb56ba2cc3254fa3b5c6c35cd54f173277a#bf941eb56ba2cc3254fa3b5c6c35cd54f173277a" } dependencies = [ { name = "aiohttp" }, { name = "cairosvg" }, @@ -2157,6 +2593,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d5/bb9997169b8b64d48f9a807fb2ec2413ff5e75c4b77612e75dd0aac8369c/suntime-1.3.2-py3-none-any.whl", hash = "sha256:33ac6ec2a3e14758cc690f7573f689d19c3131a6c9753f1bb54460bd70372ca4", size = 7211, upload-time = "2024-03-10T21:48:46.769Z" }, ] +[[package]] +name = "super-collections" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/de/a0c3d1244912c260638f0f925e190e493ccea37ecaea9bbad7c14413b803/super_collections-0.6.2.tar.gz", hash = "sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a", size = 31315, upload-time = "2025-09-30T00:37:08.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/43/47c7cf84b3bd74a8631b02d47db356656bb8dff6f2e61a4c749963814d0d/super_collections-0.6.2-py3-none-any.whl", hash = "sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56", size = 16173, upload-time = "2025-09-30T00:37:07.104Z" }, +] + [[package]] name = "tabulate" version = "0.8.10" @@ -2166,6 +2614,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/4e/e5a13fdb3e6f81ce11893523ff289870c87c8f1f289a7369fb0e9840c3bb/tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc", size = 29068, upload-time = "2022-06-21T16:26:37.943Z" }, ] +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + [[package]] name = "tinycss2" version = "1.5.1" @@ -2178,6 +2635,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "tomlkit" version = "0.13.3" @@ -2303,6 +2769,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "watchfiles" version = "1.1.1"