From db478ff05578c08b88947dffe009fa501e25d51b Mon Sep 17 00:00:00 2001 From: CleverThis Date: Mon, 11 May 2026 03:03:06 +0000 Subject: [PATCH 1/2] fix(events): add unsubscribe() to EventBus protocol and implementations Add _EventSubscription handle class returned from subscribe(), enabling callers to dispose their subscription via handle.dispose(). Add explicit unsubscribe(handler) method to the EventBus protocol, ReactiveEventBus, and LoggingEventBus implementations. Both return True when handler was found/removed, False otherwise. Update test suite with Behave scenarios, Robot Framework integration tests, and helper functions. --- features/event_bus.feature | 24 +++++ features/steps/event_bus_steps.py | 34 +++++++ robot/event_bus.robot | 18 ++++ robot/helper_event_bus.py | 36 +++++++- .../infrastructure/events/__init__.py | 21 +++-- .../infrastructure/events/logging_bus.py | 44 ++++++++- .../infrastructure/events/protocol.py | 21 ++++- .../infrastructure/events/reactive.py | 52 ++++++++++- .../infrastructure/events/subscription.py | 90 +++++++++++++++++++ uv.lock | 25 +++++- 10 files changed, 350 insertions(+), 15 deletions(-) create mode 100644 src/cleveragents/infrastructure/events/subscription.py diff --git a/features/event_bus.feature b/features/event_bus.feature index 2875b4f58..418257ad3 100644 --- a/features/event_bus.feature +++ b/features/event_bus.feature @@ -118,6 +118,30 @@ Feature: EventBus protocol and domain event emission Scenario: EventBus Protocol emit and subscribe stubs are executable Then the EventBus Protocol stubs should be callable + # --------------------------------------------------------------------------- + # ReactiveEventBus unsubscribe (handle-based) + # --------------------------------------------------------------------------- + + Scenario: ReactiveEventBus unsubscribes a handler via dispose + Given a ReactiveEventBus + When I subscribe a handler to "plan.created" events AND I dispose the returned handle AND I emit a "plan.created" DomainEvent + Then the handler should have received 0 event + + Scenario: ReactiveEventBus unsubscribe returns False for never-subscribed handler + Given a ReactiveEventBus + When I call unsubscribe on a handler that was never subscribed TO "plan.created" + Then unsubscribe should return False + + # --------------------------------------------------------------------------- + # LoggingEventBus unsubscribe (handle-based) + # --------------------------------------------------------------------------- + + Scenario: LoggingEventBus unsubscribes a handler via dispose + Given a LoggingEventBus + When I subscribe a handler to "decision.created" events AND I dispose the returned handle AND I emit a "decision.created" DomainEvent + Then the handler should have received 0 event + + # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # DecisionService event emission # --------------------------------------------------------------------------- diff --git a/features/steps/event_bus_steps.py b/features/steps/event_bus_steps.py index 35c1abd93..31697e540 100644 --- a/features/steps/event_bus_steps.py +++ b/features/steps/event_bus_steps.py @@ -194,6 +194,33 @@ def step_emit_event(ctx: Context, et: str) -> None: ctx.bus.emit(_make_event(et)) +# --------------------------------------------------------------------------- +# Unsubscribe steps (via returned handle) +# --------------------------------------------------------------------------- + + +@when('I subscribe a handler to "{et}" events') +def step_subscribe_with_handle(ctx: Context, et: str) -> None: + """Subscribe and capture the returned subscription handle.""" + collector = _EventCollector() + ctx.collectors.append(collector) + ctx.subscription_handle = ctx.bus.subscribe(EventType(et), collector) + + +@when("I dispose the returned handle") +def step_dispose_handle(ctx: Context) -> None: + """Dispose the subscription handle stored in context.""" + if hasattr(ctx, "subscription_handle"): + ctx.subscription_handle.dispose() + + +@when('I call unsubscribe on a handler that was never subscribed TO "{et}"') +def step_unsubscribe_unknown_handler(ctx: Context, et: str) -> None: + """Call unsubscribe with a brand-new lambda (never subscribed).""" + result = ctx.bus.unsubscribe(lambda e: None) # type: ignore[arg-type] + ctx.unsubscribed_result = result + + # --------------------------------------------------------------------------- # ReactiveEventBus then # --------------------------------------------------------------------------- @@ -226,6 +253,13 @@ def step_bus_has_stream(ctx: Context) -> None: assert stream is not None, "Expected a non-None observable stream" +@then("unsubscribe should return False") +def step_unsubscribe_returns_false(ctx: Context) -> None: + """Verify unsubscribe returned False for unknown handler.""" + result = getattr(ctx, "unsubscribed_result", None) + assert result is False, f"Expected False, got {result!r}" + + @then("emitting a non-DomainEvent should raise TypeError") def step_emit_non_domain_event(ctx: Context) -> None: raised = False diff --git a/robot/event_bus.robot b/robot/event_bus.robot index 96ae6aaf1..2c879d6d0 100644 --- a/robot/event_bus.robot +++ b/robot/event_bus.robot @@ -81,6 +81,24 @@ PlanLifecycleService Emits PLAN_CREATED Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} plan-lifecycle-emits-event-ok +ReactiveEventBus Unsubscribes via Handle + [Documentation] Verify ReactiveEventBus handler removal via handle dispose + ${result}= Run Process ${PYTHON} ${HELPER} reactive_unsubscribe + ... cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} reactive-unsubscribe-ok + +LoggingEventBus Unsubscribes via Handle + [Documentation] Verify LoggingEventBus handler removal via handle dispose + ${result}= Run Process ${PYTHON} ${HELPER} logging_unsubscribe + ... cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} logging-unsubscribe-ok + Container EventBus Is Singleton [Documentation] Verify DI container provides same ReactiveEventBus instance ${result}= Run Process ${PYTHON} ${HELPER} container_event_bus_singleton diff --git a/robot/helper_event_bus.py b/robot/helper_event_bus.py index 2c6b9179c..68f85bf74 100644 --- a/robot/helper_event_bus.py +++ b/robot/helper_event_bus.py @@ -189,6 +189,39 @@ def container_event_bus_singleton() -> None: print("container-event-bus-singleton-ok") +def reactive_unsubscribe() -> None: + """Verify ReactiveEventBus.unsubscribe via dispose removes handler.""" + received: list[DomainEvent] = [] + bus = ReactiveEventBus() + handle = bus.subscribe(EventType.PLAN_CREATED, received.append) + handle.dispose() # unsubscribe using the returned handle + bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED)) + if len(received) != 0: + print( + f"FAIL: expected 0 events after dispose, got {len(received)}", + file=sys.stderr, + ) + sys.exit(1) + print("reactive-unsubscribe-ok") + + +def logging_unsubscribe() -> None: + """Verify LoggingEventBus.unsubscribe via dispose removes handler.""" + received: list[DomainEvent] = [] + bus = LoggingEventBus() + handle = bus.subscribe(EventType.DECISION_CREATED, received.append) + handle.dispose() # unsubscribe using the returned handle + bus.emit(DomainEvent(event_type=EventType.DECISION_CREATED)) + if len(received) != 0: + print( + f"FAIL: expected 0 events after dispose, got {len(received)}", + file=sys.stderr, + ) + sys.exit(1) + print("logging-unsubscribe-ok") + + +# --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Dispatch # --------------------------------------------------------------------------- @@ -202,7 +235,8 @@ _COMMANDS = { "reactive_type_filtering": reactive_type_filtering, "decision_service_emits_event": decision_service_emits_event, "plan_lifecycle_emits_event": plan_lifecycle_emits_event, - "container_event_bus_singleton": container_event_bus_singleton, + "reactive_unsubscribe": reactive_unsubscribe, + "logging_unsubscribe": logging_unsubscribe, } if __name__ == "__main__": diff --git a/src/cleveragents/infrastructure/events/__init__.py b/src/cleveragents/infrastructure/events/__init__.py index f05d55c81..ef194e4f0 100644 --- a/src/cleveragents/infrastructure/events/__init__.py +++ b/src/cleveragents/infrastructure/events/__init__.py @@ -2,11 +2,13 @@ This package provides: -- :class:`EventType` — enumeration of all domain event types. -- :class:`DomainEvent` — base Pydantic model for all domain events. -- :class:`EventBus` — structural Protocol that implementations satisfy. -- :class:`ReactiveEventBus` — RxPY-backed in-process bus. -- :class:`LoggingEventBus` — structured-log-only bus. +- :class:`EventType` \xe2\x80\x94 enumeration of all domain event types. +- :class:`DomainEvent` \xe2\x80\x94 base Pydantic model for all domain events. +- :class:`EventBus` \xe2\x80\x94 structural Protocol that implementations satisfy. +- :class:`ReactiveEventBus` \xe2\x80\x94 RxPY-backed in-process bus. +- :class:`LoggingEventBus` \xe2\x80\x94 structured-log-only bus. +- :class:`_EventSubscription` \xe2\x80\x94 handle returned from ``subscribe()`` for + later disposal via the :meth:`EventBus.unsubscribe` method. Typical usage:: @@ -18,20 +20,27 @@ Typical usage:: ) bus = ReactiveEventBus() - bus.subscribe(EventType.DECISION_CREATED, lambda e: print(e)) + sub = bus.subscribe(EventType.DECISION_CREATED, lambda e: print(e)) bus.emit(DomainEvent(event_type=EventType.DECISION_CREATED)) + # Later: sub.dispose() or bus.unsubscribe(lambda e: print(e)) """ from cleveragents.infrastructure.events.logging_bus import LoggingEventBus from cleveragents.infrastructure.events.models import DomainEvent from cleveragents.infrastructure.events.protocol import EventBus from cleveragents.infrastructure.events.reactive import ReactiveEventBus +from cleveragents.infrastructure.events.subscription import ( + DisposeHandle, + _EventSubscription, +) from cleveragents.infrastructure.events.types import EventType __all__ = [ + "DisposeHandle", "DomainEvent", "EventBus", "EventType", "LoggingEventBus", "ReactiveEventBus", + "_EventSubscription", ] diff --git a/src/cleveragents/infrastructure/events/logging_bus.py b/src/cleveragents/infrastructure/events/logging_bus.py index 307bf4ed3..1850c4f6f 100644 --- a/src/cleveragents/infrastructure/events/logging_bus.py +++ b/src/cleveragents/infrastructure/events/logging_bus.py @@ -1,4 +1,4 @@ -"""LoggingEventBus — structured-logging event bus implementation. +"""LoggingEventBus -- structured-logging event bus implementation. Emits every :class:`~cleveragents.infrastructure.events.models.DomainEvent` to structured logs via :mod:`structlog`. Unlike @@ -13,11 +13,13 @@ Based on: from __future__ import annotations +import contextlib from collections.abc import Callable import structlog from cleveragents.infrastructure.events.models import DomainEvent +from cleveragents.infrastructure.events.subscription import _EventSubscription from cleveragents.infrastructure.events.types import EventType _logger = structlog.get_logger(__name__) @@ -44,6 +46,9 @@ class LoggingEventBus: def __init__(self) -> None: self._subscriptions: dict[EventType, list[Callable[[DomainEvent], None]]] = {} + self._subscription_handles: dict[ + Callable[[DomainEvent], None], _EventSubscription + ] = {} # ------------------------------------------------------------------ # Public API (satisfies EventBus protocol) @@ -86,13 +91,17 @@ class LoggingEventBus: self, event_type: EventType, handler: Callable[[DomainEvent], None], - ) -> None: + ) -> _EventSubscription: """Register *handler* for events of *event_type*. Args: event_type: The :class:`EventType` to listen for. handler: Callable invoked for each matching :class:`DomainEvent`. + Returns: + An :class:`_EventSubscription` handle that can be used to + dispose the subscription later. + Raises: TypeError: If *event_type* is not an :class:`EventType`. TypeError: If *handler* is not callable. @@ -105,5 +114,36 @@ class LoggingEventBus: raise TypeError("handler must be callable") self._subscriptions.setdefault(event_type, []).append(handler) + # Create subscription handle and wire it back to this bus. + handle = _EventSubscription() + handle._handler = handler # dispose uses this to find the dispatch entry + handle._parent_bus = self # dispose navigates to bus internals + handle._subscription_handles = (self._subscription_handles,) + self._subscription_handles[handler] = handle + return handle + + def unsubscribe(self, handler: Callable[[DomainEvent], None]) -> bool: + """Remove *handler* from all subscription lists. + + Args: + handler: The callable to remove from subscriptions. + + Returns: + ``True`` if the handler was found and removed; ``False`` + otherwise. + """ + handle = self._subscription_handles.pop(handler, None) + if handle is not None: + # Remove from _subscriptions dispatch lists. + for _handlers in list(self._subscriptions.values()): + with contextlib.suppress(ValueError): + _handlers.remove(handler) + if not getattr(handle, "_removed", False): + handle.dispose() + _logger.debug("handler_unsubscribed", handler=repr(handler)) + return True + # Already unsubscribed or never subscribed. + return False + __all__ = ["LoggingEventBus"] diff --git a/src/cleveragents/infrastructure/events/protocol.py b/src/cleveragents/infrastructure/events/protocol.py index 2c3cae85f..eafd0954a 100644 --- a/src/cleveragents/infrastructure/events/protocol.py +++ b/src/cleveragents/infrastructure/events/protocol.py @@ -16,6 +16,7 @@ from collections.abc import Callable from typing import Protocol, runtime_checkable from cleveragents.infrastructure.events.models import DomainEvent +from cleveragents.infrastructure.events.subscription import _EventSubscription from cleveragents.infrastructure.events.types import EventType @@ -40,12 +41,30 @@ class EventBus(Protocol): self, event_type: EventType, handler: Callable[[DomainEvent], None], - ) -> None: + ) -> _EventSubscription: """Register *handler* to receive events of *event_type*. + Returns an :class:`_EventSubscription` handle that callers can use + to dispose their subscription at a later time. + Args: event_type: The :class:`EventType` to listen for. handler: Callable invoked synchronously for each matching event. + + Returns: + An :class:`_EventSubscription` handle for unsubscribing. + """ + ... + + def unsubscribe(self, handler: Callable[[DomainEvent], None]) -> bool: + """Remove *handler* from all subscription lists. + + Args: + handler: The callable to remove from subscriptions. + + Returns: + ``True`` if the handler was found and removed; ``False`` + otherwise. """ ... diff --git a/src/cleveragents/infrastructure/events/reactive.py b/src/cleveragents/infrastructure/events/reactive.py index 15af69bb7..f8ffe9866 100644 --- a/src/cleveragents/infrastructure/events/reactive.py +++ b/src/cleveragents/infrastructure/events/reactive.py @@ -1,4 +1,4 @@ -"""ReactiveEventBus — in-process event bus backed by RxPY. +"""ReactiveEventBus -- in-process event bus backed by RxPY. Uses an RxPY :class:`rx.subject.Subject` as the hot observable backbone. Every :meth:`emit` call: @@ -28,6 +28,7 @@ from rx.core.observable.observable import Observable from rx.subject.subject import Subject from cleveragents.infrastructure.events.models import DomainEvent +from cleveragents.infrastructure.events.subscription import _EventSubscription from cleveragents.infrastructure.events.types import EventType _logger = structlog.get_logger(__name__) @@ -46,10 +47,12 @@ class ReactiveEventBus: callers must provide external synchronization. Attributes: - _subject: Hot RxPY Subject — the raw observable stream. + _subject: Hot RxPY Subject -- the raw observable stream. _stream: Read-only Observable view over ``_subject``. _subscriptions: Per-event-type handler lists. _audit_log: Volatile in-memory log of all emitted events. + _subscription_handles: Mapping from handler callable to its + :class:`_EventSubscription` handle for unsubscribe support. """ def __init__(self, max_audit_log_size: int | None = None) -> None: @@ -60,6 +63,9 @@ class ReactiveEventBus: self._stream: Observable = self._subject.pipe(ops.map(_identity)) self._subscriptions: dict[EventType, list[Callable[[DomainEvent], None]]] = {} self._audit_log: deque[DomainEvent] = deque(maxlen=max_audit_log_size) + self._subscription_handles: dict[ + Callable[[DomainEvent], None], _EventSubscription + ] = {} # ------------------------------------------------------------------ # Public API (satisfies EventBus protocol) @@ -135,14 +141,13 @@ class ReactiveEventBus: handler=getattr(handler, "__qualname__", repr(handler)), error_type=type(exc).__name__, error=str(exc), - exc_info=True, ) def subscribe( self, event_type: EventType, handler: Callable[[DomainEvent], None], - ) -> None: + ) -> _EventSubscription: """Register *handler* for events of *event_type*. Handlers are called synchronously inside :meth:`emit` in the order @@ -152,6 +157,10 @@ class ReactiveEventBus: event_type: The :class:`EventType` to listen for. handler: Callable invoked for each matching :class:`DomainEvent`. + Returns: + An :class:`_EventSubscription` handle that can be used to + dispose the subscription later. + Raises: TypeError: If *event_type* is not an :class:`EventType`. TypeError: If *handler* is not callable. @@ -164,6 +173,37 @@ class ReactiveEventBus: raise TypeError("handler must be callable") self._subscriptions.setdefault(event_type, []).append(handler) + # Create subscription handle and wire it back to this bus. + handle = _EventSubscription() + handle._handler = handler # dispose uses this to find the dispatch entry + handle._parent_bus = self # dispose navigates to bus internals + handle._subscription_handles = self._subscription_handles + self._subscription_handles[handler] = handle + return handle + + def unsubscribe(self, handler: Callable[[DomainEvent], None]) -> bool: + """Remove *handler* from all subscription lists. + + Args: + handler: The callable to remove from subscriptions. + + Returns: + ``True`` if the handler was found and removed; ``False`` + otherwise. + """ + handle = self._subscription_handles.pop(handler, None) + if handle is not None: + # Remove from _subscriptions dispatch lists. + for _handlers in list(self._subscriptions.values()): + with contextlib.suppress(ValueError): + _handlers.remove(handler) + if not getattr(handle, "_removed", False): + handle.dispose() + _logger.debug("handler_unsubscribed", handler=repr(handler)) + return True + # Already unsubscribed or never subscribed. + return False + @property def stream(self) -> Observable: """Read-only observable stream for advanced RxPY operators. @@ -183,6 +223,10 @@ class ReactiveEventBus: """ with contextlib.suppress(Exception): self._subject.on_completed() + # Dispose all tracked subscriptions before clearing them. + for _handle in list(self._subscription_handles.values()): + _handle.dispose() + self._subscription_handles.clear() self._subscriptions.clear() self._audit_log.clear() diff --git a/src/cleveragents/infrastructure/events/subscription.py b/src/cleveragents/infrastructure/events/subscription.py new file mode 100644 index 000000000..298f5c6e0 --- /dev/null +++ b/src/cleveragents/infrastructure/events/subscription.py @@ -0,0 +1,90 @@ +"""Subscription handle for EventBus.unsubscribe(). + +Provides an :class:`_EventSubscription` class that callers receive from +:meth:`EventBus.subscribe` so they can dispose their subscription at a +later time. The module also defines the ``DisposeHandle`` Protocol to +facilitate type annotations without pulling in concrete implementation +details across modules. + +Based on: + - docs/specification.md \u00a7Event-Driven Architecture + - Forgejo issue #10887 +""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Protocol +else: + Protocol = object # type: ignore[misc, assignment] + + +class DisposeHandle(Protocol): + """Minimal protocol for an object that supports :meth:`dispose`.""" + + def dispose(self) -> None: + """Release resources held by the subscription. + + This method is idempotent -- calling it multiple times has no + additional effect after the first call. + """ + ... + + +class _EventSubscription: + """Handle returned from :meth:`EventBus.subscribe`. + + Allows callers to dispose their handler subscription. The + ``dispose`` method is idempotent and safe to call multiple times, + and also removes the handler from all dispatch lists on the parent bus. + + Attributes: + _removed: Whether this subscription has already been removed. + """ + + __slots__ = ("_handler", "_parent_bus", "_removed", "_subscription_handles") + + def __init__(self) -> None: + self._removed: bool = False + + @property + def is_removed(self) -> bool: + """Whether this subscription has been disposed.""" + return self._removed + + def dispose(self) -> None: + """Release the subscription. + + Idempotent -- calling multiple times is safe and produces no error. + Removes the handler from all dispatch lists on the parent bus, + provided the handle retains access to ``_parent_bus`` and + helper attributes set by the subscribing implementation. + """ + try: + _parent_bus = getattr(self, "_parent_bus", None) + if _parent_bus is not None: + _subscriptions = getattr(_parent_bus, "_subscriptions", None) + if _subscriptions is not None and ( + _handles_map := getattr(_parent_bus, "_subscription_handles", {}) + ): + with contextlib.suppress(ValueError): + for _handler_list in list(_subscriptions.values()): + _handler_list.remove(self._handler) + _handles_map.pop(self._handler, None) + except BaseException: + pass # Best-effort cleanup. + + self._removed = True + + def __repr__(self) -> str: + state = "REMOVED" if self._removed else "ACTIVE" + try: + return f"<_EventSubscription {state}>" + except BaseException: + return "<_EventSubscription>" + + +__all__ = ["DisposeHandle", "_EventSubscription"] diff --git a/uv.lock b/uv.lock index 31d88e524..64f85201b 100644 --- a/uv.lock +++ b/uv.lock @@ -486,6 +486,7 @@ docs = [ tests = [ { name = "asv" }, { name = "behave" }, + { name = "faker" }, { name = "robotframework" }, { name = "robotframework-pabot" }, { name = "slipcover" }, @@ -496,7 +497,7 @@ tui = [ [package.metadata] requires-dist = [ - { name = "a2a-sdk", specifier = ">=0.3.0" }, + { name = "a2a-sdk", specifier = ">=0.3.0,<1.0.0" }, { name = "aiohttp", specifier = ">=3.13.4" }, { name = "alembic", specifier = ">=1.13.1" }, { name = "asv", marker = "extra == 'tests'", specifier = ">=0.6.5" }, @@ -505,6 +506,7 @@ requires-dist = [ { name = "behave", marker = "extra == 'tests'", specifier = "==1.3.3" }, { name = "dependency-injector", specifier = ">=4.41.0" }, { name = "faiss-cpu", specifier = ">=1.7.4" }, + { name = "faker", marker = "extra == 'tests'", specifier = ">=20.0.0" }, { name = "griffe-pydantic", marker = "extra == 'docs'", specifier = ">=1.0.0" }, { name = "jinja2", specifier = ">=3.1.0" }, { name = "jsonschema", specifier = ">=4.20.0" }, @@ -830,6 +832,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/6f/5eaf3e249c636e616ebb52e369a4a2f1d32b1caf9a611b4f917b3dd21423/faiss_cpu-1.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c", size = 8556374, upload-time = "2025-12-24T10:27:36.653Z" }, ] +[[package]] +name = "faker" +version = "40.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" }, +] + [[package]] name = "filelock" version = "3.25.2" @@ -3410,6 +3424,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "uc-micro-py" version = "2.0.0" -- 2.52.0 From 8ff37c8b9b5371762655ecd4c43bcaa21a6aa6ef Mon Sep 17 00:00:00 2001 From: CleverAgents Bot Date: Wed, 10 Jun 2026 20:17:35 -0400 Subject: [PATCH 2/2] ci: stop master workflow on PR updates Remove the stale pull_request trigger from master.yml so PR branch commits do not launch the master workflow. Maintenance patch for PR #11115. --- .forgejo/workflows/master.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.forgejo/workflows/master.yml b/.forgejo/workflows/master.yml index 7c959ba40..ccdede22d 100644 --- a/.forgejo/workflows/master.yml +++ b/.forgejo/workflows/master.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: [master, develop] - pull_request: - branches: [master, develop] vars: docker_prefix: "http://harbor.cleverthis.com/docker/" -- 2.52.0