Conversation
📝 WalkthroughWalkthroughAdds a new jumpstarter-driver-xcp package (driver, client, models), test harnesses (mock server/master, extensive tests), an XCP ECU example with a stateful mock and integration tests, packaging/pyproject files, example configs, and documentation entries. No breaking changes to existing public APIs. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant XcpDriver
participant PyXCPMaster
participant SlaveECU
Client->>XcpDriver: connect(mode=0)
XcpDriver->>PyXCPMaster: master.connect(mode)
PyXCPMaster->>SlaveECU: XCP CONNECT
SlaveECU-->>PyXCPMaster: slaveProperties
PyXCPMaster-->>XcpDriver: connection info
XcpDriver-->>Client: XcpConnectionInfo
sequenceDiagram
participant Client
participant XcpDriver
participant PyXCPMaster
participant SlaveECU
Client->>XcpDriver: set_mta(address)
XcpDriver->>PyXCPMaster: master.setMta(address)
PyXCPMaster->>SlaveECU: XCP SET_MTA
SlaveECU-->>PyXCPMaster: ack
Client->>XcpDriver: upload(length,address)
XcpDriver->>PyXCPMaster: master.shortUpload(length,address)
PyXCPMaster->>SlaveECU: XCP SHORT_UPLOAD
SlaveECU-->>PyXCPMaster: data
PyXCPMaster-->>XcpDriver: bytes
XcpDriver-->>Client: data
sequenceDiagram
participant Client
participant XcpDriver
participant PyXCPMaster
participant SlaveECU
Client->>XcpDriver: program_start()
XcpDriver->>PyXCPMaster: master.programStart()
PyXCPMaster->>SlaveECU: XCP PROGRAM_START
SlaveECU-->>PyXCPMaster: program info
Client->>XcpDriver: program_clear(range)
XcpDriver->>PyXCPMaster: master.programClear(range)
PyXCPMaster->>SlaveECU: XCP PROGRAM_CLEAR
SlaveECU-->>PyXCPMaster: ack
Client->>XcpDriver: program(data)
loop per block
XcpDriver->>PyXCPMaster: master.program(block)
PyXCPMaster->>SlaveECU: XCP PROGRAM (block)
SlaveECU-->>PyXCPMaster: ack
end
Client->>XcpDriver: program_reset()
XcpDriver->>PyXCPMaster: master.programReset()
PyXCPMaster->>SlaveECU: XCP PROGRAM_RESET
SlaveECU-->>PyXCPMaster: ack
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can generate a title for your PR based on the changes with custom instructions.Set the |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver_test.py (1)
411-426: Simplify argument extraction logic.This complex argument extraction is fragile and hard to maintain. Since you're already checking
call_kwargs.kwargsat line 429, the intermediate logic (lines 411-426) appears unused.♻️ Suggested simplification
mock_create.assert_called_once() - call_kwargs = mock_create.call_args - args = call_kwargs.kwargs if call_kwargs.kwargs else {} - if not args: - args = { - "transport": call_kwargs[1].get("transport", call_kwargs[0][0] if call_kwargs[0] else None), - } - # Extract from positional/keyword args - keys = [ - "transport", "config_file", "host", "port", "protocol", - "can_interface", "channel", "bitrate", "can_id_master", "can_id_slave", - ] - for i, key in enumerate(keys): - if i < len(call_kwargs[0]): - args[key] = call_kwargs[0][i] - elif key in (call_kwargs[1] if len(call_kwargs) > 1 and call_kwargs[1] else {}): - args[key] = call_kwargs[1][key] - # The call should use the CAN transport with our specific params actual_kwargs = mock_create.call_args.kwargs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver_test.py` around lines 411 - 426, The current fragile argument-extraction block around call_kwargs and mock_create (variables call_kwargs, mock_create, args) reconstructs positional and keyword args manually and is redundant because you already use call_kwargs.kwargs later; remove the entire manual extraction (the block that builds args from call_kwargs[0] and call_kwargs[1] and the keys list) and instead set args directly from call_kwargs.kwargs or call_kwargs.args/kwargs in a simple, consistent way (e.g., if call_kwargs.kwargs: args = call_kwargs.kwargs else: map positional call_kwargs.args to expected parameter names), ensuring subsequent code uses this single args mapping.python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/conftest.py (1)
417-423: Verifymax()argument order for intended behavior.
max(self._daq_lists, 4)returns the larger of the two values. If the intent is "at least 4 DAQ lists available", this is correct. However, if_daq_listscould be 0 (afterfreeDaq()), this still returns 4, which may not accurately reflect the current allocation state.💡 Consider clarifying the intent
If the mock should report actual allocation after
freeDaq():- "processor": {"minDaq": 0, "maxDaq": max(self._daq_lists, 4)}, + "processor": {"minDaq": 0, "maxDaq": self._daq_lists if self._daq_lists > 0 else 4},Or add a comment clarifying that 4 is the hardware capability:
+ # maxDaq reflects hardware capability (4) or current allocation "processor": {"minDaq": 0, "maxDaq": max(self._daq_lists, 4)},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/conftest.py` around lines 417 - 423, The getDaqInfo method uses max(self._daq_lists, 4) which is ambiguous; decide and make intent explicit: if you want the mock to reflect actual allocation after freeDaq(), return "maxDaq": self._daq_lists (remove the max), otherwise if 4 represents a hardware minimum keep the max but switch to max(4, self._daq_lists) and add a clarifying comment; locate getDaqInfo and the _daq_lists field to implement the chosen change and update the comment accordingly.python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.py (1)
58-107: Thread-safety limitation: concurrent driver instantiation is unsafe due topyxcp.cmdline.ArgumentParsermutating process-global state.The
sys.argvsave/restore is correctly implemented, butpyxcp.cmdline.ArgumentParseris fundamentally not thread-safe: its constructor modifiessys.argvin-place viaparse_and_strip(), and.run()manages a process-global singleton without locking. Concurrent instantiation of multipleXcpdrivers will cause race conditions.Document this clearly in the docstring and class documentation:
- Ensure
Xcpdriver instances are created from a single thread during initialization.- If concurrent instantiation is required, serialize driver creation with an explicit lock around the entire
_create_xcp_master()function call.Alternatively, consider adopting
pyxcp.config.create_application_from_config()for programmatic configuration to avoid the ArgumentParser CLI entirely.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.py` around lines 58 - 107, The _create_xcp_master function relies on pyxcp.cmdline.ArgumentParser (XcpArgumentParser) which mutates process-global state (it calls parse_and_strip on sys.argv and uses a global singleton via .run()), making concurrent instantiation of drivers unsafe; update the _create_xcp_master docstring and any class docs for the Xcp driver to explicitly state that driver instances must be created from a single thread (or that callers must serialize calls to _create_xcp_master with an external lock), and add a note suggesting switching to pyxcp.config.create_application_from_config() for programmatic, thread-safe creation instead of using XcpArgumentParser.python/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.py (1)
93-107:program_clear()isn't being verified through observable behavior.The flash is already asserted to be erased before
program_clear(), so this test mainly proves that_program_clearedflipped. A no-op clear implementation would still pass here. Seed non-erased bytes first, or assert thatprogram_clear()restores an area after it was dirtied.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@python/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.py` around lines 93 - 107, The test currently only checks that mock_ecu._program_cleared flipped; instead seed the flash with non-erased data at FLASH_BASE (e.g., write a non-zero pattern using the ECU client's write/program method or directly manipulate mock_ecu storage), verify upload(4, FLASH_BASE, 0) returns that non-zero data, then call ecu_client.program_clear(FLASH_SIZE) and assert that subsequent _to_bytes(ecu_client.upload(4, FLASH_BASE, 0)) returns all zeros and that mock_ecu._program_cleared is True; reference ecu_client.upload, ecu_client.program_clear, FLASH_BASE and FLASH_SIZE to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@python/examples/xcp-ecu/jumpstarter_example_xcp_ecu/mock_ecu.py`:
- Around line 298-303: The flash erased state is inconsistent: flash is
initialized elsewhere using b"\x00" but program() falls back to b"\xFF"; update
the fallback in program() to use the same erased byte as initialization (replace
b"\xFF" with b"\x00") or, better, reference a single constant (e.g.,
FLASH_ERASED_BYTE) so FLASH_BASE, FLASH_SIZE and self._memory use the same
erased-state value across initialization and the program() write path.
In `@python/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.py`:
- Around line 223-233: The test test_calibration_write_without_unlock_succeeds
currently asserts that a calibration write succeeds without unlocking which
contradicts the README and weakens the protection flow; update the test so it
requires calling ecu_client.unlock() (or otherwise set
mock_ecu._protection["calpag"] = False via the existing unlock flow) before
performing ecu_client.download and ecu_client.upload, and then assert the write
persists, and adjust any checks that inspect mock_ecu._protection to reflect
that unlock() must be called for calibration writes to succeed.
---
Nitpick comments:
In `@python/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.py`:
- Around line 93-107: The test currently only checks that
mock_ecu._program_cleared flipped; instead seed the flash with non-erased data
at FLASH_BASE (e.g., write a non-zero pattern using the ECU client's
write/program method or directly manipulate mock_ecu storage), verify upload(4,
FLASH_BASE, 0) returns that non-zero data, then call
ecu_client.program_clear(FLASH_SIZE) and assert that subsequent
_to_bytes(ecu_client.upload(4, FLASH_BASE, 0)) returns all zeros and that
mock_ecu._program_cleared is True; reference ecu_client.upload,
ecu_client.program_clear, FLASH_BASE and FLASH_SIZE to locate the change.
In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/conftest.py`:
- Around line 417-423: The getDaqInfo method uses max(self._daq_lists, 4) which
is ambiguous; decide and make intent explicit: if you want the mock to reflect
actual allocation after freeDaq(), return "maxDaq": self._daq_lists (remove the
max), otherwise if 4 represents a hardware minimum keep the max but switch to
max(4, self._daq_lists) and add a clarifying comment; locate getDaqInfo and the
_daq_lists field to implement the chosen change and update the comment
accordingly.
In
`@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver_test.py`:
- Around line 411-426: The current fragile argument-extraction block around
call_kwargs and mock_create (variables call_kwargs, mock_create, args)
reconstructs positional and keyword args manually and is redundant because you
already use call_kwargs.kwargs later; remove the entire manual extraction (the
block that builds args from call_kwargs[0] and call_kwargs[1] and the keys list)
and instead set args directly from call_kwargs.kwargs or call_kwargs.args/kwargs
in a simple, consistent way (e.g., if call_kwargs.kwargs: args =
call_kwargs.kwargs else: map positional call_kwargs.args to expected parameter
names), ensuring subsequent code uses this single args mapping.
In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.py`:
- Around line 58-107: The _create_xcp_master function relies on
pyxcp.cmdline.ArgumentParser (XcpArgumentParser) which mutates process-global
state (it calls parse_and_strip on sys.argv and uses a global singleton via
.run()), making concurrent instantiation of drivers unsafe; update the
_create_xcp_master docstring and any class docs for the Xcp driver to explicitly
state that driver instances must be created from a single thread (or that
callers must serialize calls to _create_xcp_master with an external lock), and
add a note suggesting switching to pyxcp.config.create_application_from_config()
for programmatic, thread-safe creation instead of using XcpArgumentParser.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4383504c-7554-4c96-aade-148b97b44613
⛔ Files ignored due to path filters (1)
python/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (18)
python/docs/source/reference/package-apis/drivers/xcp.mdpython/examples/xcp-ecu/README.mdpython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/__init__.pypython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/conftest.pypython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/mock_ecu.pypython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.pypython/examples/xcp-ecu/pyproject.tomlpython/packages/jumpstarter-driver-xcp/.gitignorepython/packages/jumpstarter-driver-xcp/README.mdpython/packages/jumpstarter-driver-xcp/examples/exporter.yamlpython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/__init__.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/client.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/common.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/conftest.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver_test.pypython/packages/jumpstarter-driver-xcp/pyproject.tomlpython/pyproject.toml
python/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.py
Outdated
Show resolved
Hide resolved
✅ Deploy Preview for jumpstarter-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Addressed all coderabbit comments. |
Wraps the pyXCP library to provide remote measurement, calibration, DAQ, and programming of XCP-enabled ECUs over Ethernet, CAN, USB, and Serial transports.
Add StatefulXcpMaster to driver tests (16 new tests) verifying connection state, memory round-trips, DAQ allocation, programming sequence enforcement, and reconnect behavior through the gRPC boundary. Add python/examples/xcp-ecu/ example package (14 scenario tests) modeled after the automotive UDS example, with a mock ECU that simulates calibration parameters, measurement signals, flash programming, and resource protection.
- Fix flash erased state inconsistency in mock_ecu: introduce FLASH_ERASED_BYTE constant and use it consistently across init, programClear, and program (was using b"\xFF" fallback vs b"\x00") - Enforce CAL/PAG protection on mock ECU downloads and update test_calibration_write to require unlock before writing - Improve test_full_flash_programming_workflow to seed non-erased data before program_clear, verifying erasure actually works - Clarify getDaqInfo max() intent with reordered args and comment - Remove dead argument-extraction code in test_custom_config_forwarded - Add thread-safety warning to _create_xcp_master docstring
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.py (1)
58-112: Thread-safety caveat is well-documented, but consider adding a lock.The warning about
sys.argvmanipulation and pyxcp's ArgumentParser not being thread-safe is clear (lines 75-80). However, if multipleXcpdriver instances are created concurrently (e.g., in a multi-threaded test runner or when multiple drivers are served simultaneously), thesys.argvmanipulation could cause race conditions.Consider adding a module-level lock to serialize master creation:
♻️ Optional: Add lock for thread safety
+import threading + +_create_master_lock = threading.Lock() + def _create_xcp_master( transport: XcpTransport, ... ): ... + with _create_master_lock: saved_argv = sys.argv sys.argv = [ "jumpstarter-xcp", "-t", transport.value.lower(), "--config", cfg_path, ] try: ap = XcpArgumentParser(description="Jumpstarter XCP Driver") master = ap.run() finally: sys.argv = saved_argv🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.py` around lines 58 - 112, Add a module-level threading.Lock (e.g., _xcp_master_lock) and acquire it around the critical section in _create_xcp_master that mutates sys.argv and invokes XcpArgumentParser.run(), releasing it in the outer finally to ensure serialization; keep tmp_path creation and cleanup as-is and ensure saved_argv restoration and master creation remain inside the locked region so sys.argv is never concurrently modified while XcpArgumentParser is parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@python/packages/jumpstarter-driver-xcp/pyproject.toml`:
- Around line 27-45: The pytest config sets asyncio_mode = "auto" but
pytest-asyncio is not declared in the dev dependency group; add
"pytest-asyncio>=0.20.0" (or compatible minimum) to the dependency-groups dev
list so pytest can run async tests, updating the dev group in pyproject.toml
where dependency-groups and asyncio_mode are defined and ensuring the package is
installed in CI/dev environments.
---
Nitpick comments:
In `@python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.py`:
- Around line 58-112: Add a module-level threading.Lock (e.g., _xcp_master_lock)
and acquire it around the critical section in _create_xcp_master that mutates
sys.argv and invokes XcpArgumentParser.run(), releasing it in the outer finally
to ensure serialization; keep tmp_path creation and cleanup as-is and ensure
saved_argv restoration and master creation remain inside the locked region so
sys.argv is never concurrently modified while XcpArgumentParser is parsing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 99dcc71d-36ef-4d2e-89e5-efa0af571549
⛔ Files ignored due to path filters (1)
python/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
python/docs/source/reference/package-apis/drivers/index.mdpython/docs/source/reference/package-apis/drivers/xcp.mdpython/examples/xcp-ecu/README.mdpython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/__init__.pypython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/conftest.pypython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/mock_ecu.pypython/examples/xcp-ecu/jumpstarter_example_xcp_ecu/test_xcp_flow.pypython/examples/xcp-ecu/pyproject.tomlpython/packages/jumpstarter-driver-xcp/.gitignorepython/packages/jumpstarter-driver-xcp/README.mdpython/packages/jumpstarter-driver-xcp/examples/exporter.yamlpython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/__init__.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/client.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/common.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/conftest.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver.pypython/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/driver_test.pypython/packages/jumpstarter-driver-xcp/pyproject.tomlpython/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (8)
- python/packages/jumpstarter-driver-xcp/README.md
- python/packages/jumpstarter-driver-xcp/.gitignore
- python/packages/jumpstarter-driver-xcp/examples/exporter.yaml
- python/docs/source/reference/package-apis/drivers/xcp.md
- python/docs/source/reference/package-apis/drivers/index.md
- python/pyproject.toml
- python/packages/jumpstarter-driver-xcp/jumpstarter_driver_xcp/common.py
- python/examples/xcp-ecu/README.md
| [tool.pytest.ini_options] | ||
| addopts = "--cov --cov-report=html --cov-report=xml" | ||
| log_cli = true | ||
| log_cli_level = "INFO" | ||
| testpaths = ["jumpstarter_driver_xcp"] | ||
| asyncio_mode = "auto" | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling", "hatch-vcs", "hatch-pin-jumpstarter"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.hooks.pin_jumpstarter] | ||
| name = "pin_jumpstarter" | ||
|
|
||
| [dependency-groups] | ||
| dev = [ | ||
| "pytest-cov>=6.0.0", | ||
| "pytest>=8.3.3", | ||
| ] |
There was a problem hiding this comment.
Missing pytest-asyncio dependency for asyncio_mode = "auto".
The pytest configuration specifies asyncio_mode = "auto" (line 32), which requires pytest-asyncio to be installed. However, it's not listed in the dev dependencies.
🛠️ Proposed fix
[dependency-groups]
dev = [
"pytest-cov>=6.0.0",
"pytest>=8.3.3",
+ "pytest-asyncio>=0.23.0",
]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [tool.pytest.ini_options] | |
| addopts = "--cov --cov-report=html --cov-report=xml" | |
| log_cli = true | |
| log_cli_level = "INFO" | |
| testpaths = ["jumpstarter_driver_xcp"] | |
| asyncio_mode = "auto" | |
| [build-system] | |
| requires = ["hatchling", "hatch-vcs", "hatch-pin-jumpstarter"] | |
| build-backend = "hatchling.build" | |
| [tool.hatch.build.hooks.pin_jumpstarter] | |
| name = "pin_jumpstarter" | |
| [dependency-groups] | |
| dev = [ | |
| "pytest-cov>=6.0.0", | |
| "pytest>=8.3.3", | |
| ] | |
| [tool.pytest.ini_options] | |
| addopts = "--cov --cov-report=html --cov-report=xml" | |
| log_cli = true | |
| log_cli_level = "INFO" | |
| testpaths = ["jumpstarter_driver_xcp"] | |
| asyncio_mode = "auto" | |
| [build-system] | |
| requires = ["hatchling", "hatch-vcs", "hatch-pin-jumpstarter"] | |
| build-backend = "hatchling.build" | |
| [tool.hatch.build.hooks.pin_jumpstarter] | |
| name = "pin_jumpstarter" | |
| [dependency-groups] | |
| dev = [ | |
| "pytest-cov>=6.0.0", | |
| "pytest>=8.3.3", | |
| "pytest-asyncio>=0.23.0", | |
| ] |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@python/packages/jumpstarter-driver-xcp/pyproject.toml` around lines 27 - 45,
The pytest config sets asyncio_mode = "auto" but pytest-asyncio is not declared
in the dev dependency group; add "pytest-asyncio>=0.20.0" (or compatible
minimum) to the dependency-groups dev list so pytest can run async tests,
updating the dev group in pyproject.toml where dependency-groups and
asyncio_mode are defined and ensuring the package is installed in CI/dev
environments.
Summary by CodeRabbit
New Features
Examples
Documentation
Tests
Chores