agentspype 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentspype-0.1.0/LICENSE +21 -0
- agentspype-0.1.0/PKG-INFO +19 -0
- agentspype-0.1.0/README.md +3 -0
- agentspype-0.1.0/agentspype/__init__.py +0 -0
- agentspype-0.1.0/agentspype/agency.py +31 -0
- agentspype-0.1.0/agentspype/agent/__init__.py +0 -0
- agentspype-0.1.0/agentspype/agent/agent.py +79 -0
- agentspype-0.1.0/agentspype/agent/configuration.py +7 -0
- agentspype-0.1.0/agentspype/agent/definition.py +19 -0
- agentspype-0.1.0/agentspype/agent/listening.py +33 -0
- agentspype-0.1.0/agentspype/agent/publishing.py +56 -0
- agentspype-0.1.0/agentspype/agent/state_machine.py +134 -0
- agentspype-0.1.0/agentspype/agent/status.py +7 -0
- agentspype-0.1.0/pyproject.toml +72 -0
agentspype-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Gianluca Pagliara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: agentspype
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A framework for building agents that interact with the world
|
|
5
|
+
Author: Gianluca Pagliara
|
|
6
|
+
Author-email: pagliara.gianluca@gmail.com
|
|
7
|
+
Requires-Python: >=3.13,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Dist: eventspype (>=1.0.0,<2.0.0)
|
|
11
|
+
Requires-Dist: pydantic (>=2.10.4,<3.0.0)
|
|
12
|
+
Requires-Dist: pydot (>=3.0.3,<4.0.0)
|
|
13
|
+
Requires-Dist: python-statemachine (>=2.5.0,<3.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Agents Pypeline
|
|
17
|
+
|
|
18
|
+
A framework for building agents that interact with the world.
|
|
19
|
+
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from .agent.agent import Agent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Agency:
|
|
8
|
+
initialized_agents: list["Agent"] = []
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def register_agent(cls, agent: "Agent") -> None:
|
|
12
|
+
"""Register an initialized agent."""
|
|
13
|
+
if agent not in cls.initialized_agents:
|
|
14
|
+
agent.logger().info(f"[Agency] Registered: {agent.__class__.__name__}")
|
|
15
|
+
cls.initialized_agents.append(agent)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def deregister_agent(cls, agent: "Agent") -> None:
|
|
19
|
+
"""Deregister an initialized agent."""
|
|
20
|
+
if agent in cls.initialized_agents:
|
|
21
|
+
agent.logger().info(f"[Agency] Deregistered: {agent.__class__.__name__}")
|
|
22
|
+
cls.initialized_agents.remove(agent)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_active_agents(cls) -> list["Agent"]:
|
|
26
|
+
"""Get a list of all active agents."""
|
|
27
|
+
return [
|
|
28
|
+
agent
|
|
29
|
+
for agent in cls.initialized_agents
|
|
30
|
+
if not agent.machine.current_state.final
|
|
31
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from ..agency import Agency
|
|
5
|
+
from .configuration import AgentConfiguration
|
|
6
|
+
from .definition import AgentDefinition
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .listening import AgentListening
|
|
10
|
+
from .publishing import AgentPublishing
|
|
11
|
+
from .state_machine import AgentStateMachine
|
|
12
|
+
from .status import AgentStatus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Agent:
|
|
16
|
+
_logger: logging.Logger | None = None
|
|
17
|
+
|
|
18
|
+
# === Definition ===
|
|
19
|
+
|
|
20
|
+
definition: AgentDefinition
|
|
21
|
+
|
|
22
|
+
# === Initialization ===
|
|
23
|
+
|
|
24
|
+
def __init__(self, configuration: AgentConfiguration | dict[str, Any]):
|
|
25
|
+
self._configuration = (
|
|
26
|
+
configuration
|
|
27
|
+
if isinstance(configuration, AgentConfiguration)
|
|
28
|
+
else self.definition.configuration_class(**configuration)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
self._events_publishing = self.definition.events_publishing_class(self)
|
|
32
|
+
self._events_listening = self.definition.events_listening_class(self)
|
|
33
|
+
self._state_machine = self.definition.state_machine_class(self)
|
|
34
|
+
self._status = self.definition.status_class()
|
|
35
|
+
|
|
36
|
+
Agency.register_agent(self)
|
|
37
|
+
|
|
38
|
+
self.initialize()
|
|
39
|
+
|
|
40
|
+
def initialize(self) -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def teardown(self) -> None:
|
|
44
|
+
self.listening.unsubscribe()
|
|
45
|
+
|
|
46
|
+
Agency.deregister_agent(self)
|
|
47
|
+
|
|
48
|
+
def clone(self) -> "Agent":
|
|
49
|
+
return self.__class__(self.configuration)
|
|
50
|
+
|
|
51
|
+
# === Class Methods ===
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def logger(cls) -> logging.Logger:
|
|
55
|
+
if cls._logger is None:
|
|
56
|
+
cls._logger = logging.getLogger("agent")
|
|
57
|
+
return cls._logger
|
|
58
|
+
|
|
59
|
+
# === Properties ===
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def configuration(self) -> AgentConfiguration:
|
|
63
|
+
return self._configuration
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def machine(self) -> "AgentStateMachine":
|
|
67
|
+
return self._state_machine
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def listening(self) -> "AgentListening":
|
|
71
|
+
return self._events_listening
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def publishing(self) -> "AgentPublishing":
|
|
75
|
+
return self._events_publishing
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def status(self) -> "AgentStatus":
|
|
79
|
+
return self._status
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
|
|
3
|
+
from .configuration import AgentConfiguration
|
|
4
|
+
from .listening import AgentListening
|
|
5
|
+
from .publishing import AgentPublishing
|
|
6
|
+
from .state_machine import AgentStateMachine
|
|
7
|
+
from .status import AgentStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentDefinition(BaseModel):
|
|
11
|
+
"""Definition of an agent's components and configuration."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(frozen=True)
|
|
14
|
+
|
|
15
|
+
state_machine_class: type[AgentStateMachine]
|
|
16
|
+
events_listening_class: type[AgentListening]
|
|
17
|
+
events_publishing_class: type[AgentPublishing]
|
|
18
|
+
configuration_class: type[AgentConfiguration]
|
|
19
|
+
status_class: type[AgentStatus]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from eventspype.sub.multisubscriber import MultiSubscriber
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .agent import Agent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AgentListening(MultiSubscriber):
|
|
12
|
+
def __init__(self, agent: "Agent") -> None:
|
|
13
|
+
super().__init__()
|
|
14
|
+
self._agent = agent
|
|
15
|
+
|
|
16
|
+
# === Properties ===
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def agent(self) -> "Agent":
|
|
20
|
+
return self._agent
|
|
21
|
+
|
|
22
|
+
def logger(self) -> logging.Logger:
|
|
23
|
+
return self.agent.logger()
|
|
24
|
+
|
|
25
|
+
# === Subscriptions ===
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def subscribe(self) -> None:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def unsubscribe(self) -> None:
|
|
33
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from eventspype.pub.multipublisher import MultiPublisher
|
|
6
|
+
from eventspype.pub.publication import EventPublication
|
|
7
|
+
from statemachine import State
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .agent import Agent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentPublishing(MultiPublisher):
|
|
14
|
+
def __init__(self, agent: "Agent") -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self._agent = agent
|
|
17
|
+
|
|
18
|
+
# === Properties ===
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def agent(self) -> "Agent":
|
|
22
|
+
return self._agent
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StateAgentPublishing(AgentPublishing):
|
|
26
|
+
# === Definitions ===
|
|
27
|
+
|
|
28
|
+
class Events(Enum):
|
|
29
|
+
StateMachineTransition = "sm_transition_event"
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class StateMachineEvent:
|
|
33
|
+
event: Any
|
|
34
|
+
new_state: State
|
|
35
|
+
|
|
36
|
+
# === Publications ===
|
|
37
|
+
|
|
38
|
+
sm_transition_event = EventPublication(
|
|
39
|
+
event_tag=Events.StateMachineTransition, event_class=StateMachineEvent
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# === Events ===
|
|
43
|
+
|
|
44
|
+
def publish_transition(self, event: Any, state: State) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Publishes a state machine transition event.
|
|
47
|
+
|
|
48
|
+
It is recommended to call this method from the state machine.
|
|
49
|
+
Example:
|
|
50
|
+
def after_transition(self, event, state):
|
|
51
|
+
self.agent.publishing.publish_transition(event, state)
|
|
52
|
+
"""
|
|
53
|
+
self.trigger_event(
|
|
54
|
+
self.sm_transition_event,
|
|
55
|
+
self.sm_transition_event.event_class(event, state),
|
|
56
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
|
|
3
|
+
|
|
4
|
+
from statemachine import State, StateMachine
|
|
5
|
+
from statemachine.factory import StateMachineMetaclass
|
|
6
|
+
from statemachine.transition_list import TransitionList
|
|
7
|
+
|
|
8
|
+
from .publishing import StateAgentPublishing
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .agent import Agent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T", bound="AgentStateMachine")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentStateMachineMeta(StateMachineMetaclass):
|
|
18
|
+
"""Metaclass for AgentStateMachine that ensures proper state machine inheritance."""
|
|
19
|
+
|
|
20
|
+
def __new__(
|
|
21
|
+
mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
|
|
22
|
+
) -> type:
|
|
23
|
+
# Don't modify the base class
|
|
24
|
+
if name == "AgentStateMachine":
|
|
25
|
+
return super().__new__(mcs, name, bases, namespace)
|
|
26
|
+
|
|
27
|
+
# Create state definitions if not already defined
|
|
28
|
+
if "starting" not in namespace:
|
|
29
|
+
namespace["starting"] = State("Starting", initial=True)
|
|
30
|
+
if "idle" not in namespace:
|
|
31
|
+
namespace["idle"] = State("Idle")
|
|
32
|
+
if "end" not in namespace:
|
|
33
|
+
namespace["end"] = State("End", final=True)
|
|
34
|
+
|
|
35
|
+
# Create transition definitions if not already defined
|
|
36
|
+
if "start" not in namespace:
|
|
37
|
+
namespace["start"] = namespace["starting"].to(namespace["idle"])
|
|
38
|
+
if "stop" not in namespace:
|
|
39
|
+
namespace["stop"] = namespace["starting"].to(namespace["end"]) | namespace[
|
|
40
|
+
"idle"
|
|
41
|
+
].to(namespace["end"])
|
|
42
|
+
|
|
43
|
+
return super().__new__(mcs, name, bases, namespace)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentStateMachine(StateMachine, metaclass=AgentStateMachineMeta):
|
|
47
|
+
"""Base class for all agent state machines.
|
|
48
|
+
|
|
49
|
+
This class provides the basic state machine structure that all agents should inherit from.
|
|
50
|
+
Child classes can override states and transitions by defining their own, or inherit the defaults.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# === States ===
|
|
54
|
+
|
|
55
|
+
starting: ClassVar[State]
|
|
56
|
+
idle: ClassVar[State]
|
|
57
|
+
end: ClassVar[State]
|
|
58
|
+
|
|
59
|
+
# === Transitions ===
|
|
60
|
+
|
|
61
|
+
start: ClassVar[TransitionList]
|
|
62
|
+
stop: ClassVar[TransitionList]
|
|
63
|
+
|
|
64
|
+
def __init__(self, agent: "Agent") -> None:
|
|
65
|
+
super().__init__()
|
|
66
|
+
self._agent = agent
|
|
67
|
+
self._should_stop = False
|
|
68
|
+
|
|
69
|
+
# === State actions ===
|
|
70
|
+
|
|
71
|
+
def on_enter_end(self) -> None:
|
|
72
|
+
self.agent.teardown()
|
|
73
|
+
|
|
74
|
+
# === Transitions Actions ===
|
|
75
|
+
|
|
76
|
+
def before_transition(
|
|
77
|
+
self, event: str, state: str, source: str, target: str
|
|
78
|
+
) -> None:
|
|
79
|
+
if source == target:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
self.agent.logger().debug(
|
|
83
|
+
f"[{self.agent.__class__.__name__}:StateMachine] ({event}) {source} -> {target}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def after_transition(self, event: str, state: State) -> None:
|
|
88
|
+
"""Handle behavior after a transition.
|
|
89
|
+
|
|
90
|
+
This method should be implemented in child classes to handle post-transition behavior.
|
|
91
|
+
Example implementation:
|
|
92
|
+
self.agent.publishing.publish_transition(event, state)
|
|
93
|
+
"""
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
def on_start(self) -> None:
|
|
97
|
+
self.agent.listening.subscribe()
|
|
98
|
+
|
|
99
|
+
def on_stop(self) -> None:
|
|
100
|
+
self._should_stop = True
|
|
101
|
+
|
|
102
|
+
# === Conditions ===
|
|
103
|
+
|
|
104
|
+
def should_stop(self) -> bool:
|
|
105
|
+
return self._should_stop
|
|
106
|
+
|
|
107
|
+
# === Properties ===
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def agent(self) -> "Agent":
|
|
111
|
+
return self._agent
|
|
112
|
+
|
|
113
|
+
# === Functions ===
|
|
114
|
+
|
|
115
|
+
def safe_stop(self) -> None:
|
|
116
|
+
"""Safely stop the state machine if not already in final state."""
|
|
117
|
+
if self.current_state.final:
|
|
118
|
+
return
|
|
119
|
+
self.stop(f=True)
|
|
120
|
+
|
|
121
|
+
# === Diagrams ===
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Example of how to create a concrete state machine:
|
|
125
|
+
class BasicAgentStateMachine(AgentStateMachine):
|
|
126
|
+
"""A basic implementation of an agent state machine.
|
|
127
|
+
|
|
128
|
+
This class inherits all the default states and transitions from AgentStateMachine.
|
|
129
|
+
It only needs to implement the required after_transition method.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def after_transition(self, event: str, state: State) -> None:
|
|
133
|
+
if isinstance(self.agent.publishing, StateAgentPublishing):
|
|
134
|
+
self.agent.publishing.publish_transition(event, state)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "agentspype"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A framework for building agents that interact with the world"
|
|
5
|
+
authors = ["Gianluca Pagliara <pagliara.gianluca@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.13"
|
|
10
|
+
pydantic = "^2.10.4"
|
|
11
|
+
python-statemachine = "^2.5.0"
|
|
12
|
+
pydot = "^3.0.3"
|
|
13
|
+
eventspype = "^1.0.0"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.dev.dependencies]
|
|
16
|
+
pytest = "^7.4.3"
|
|
17
|
+
pytest-cov = "^4.1.0"
|
|
18
|
+
black = "^23.11.0"
|
|
19
|
+
isort = "^5.12.0"
|
|
20
|
+
mypy = "^1.7.0"
|
|
21
|
+
safety = "^2.3.5"
|
|
22
|
+
pre-commit = "^3.5.0"
|
|
23
|
+
ruff = "^0.8.4"
|
|
24
|
+
pytest-asyncio = "<0.25.0"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["poetry-core"]
|
|
28
|
+
build-backend = "poetry.core.masonry.api"
|
|
29
|
+
|
|
30
|
+
[tool.black]
|
|
31
|
+
line-length = 88
|
|
32
|
+
target-version = ['py312']
|
|
33
|
+
include = '\.pyi?$'
|
|
34
|
+
|
|
35
|
+
[tool.isort]
|
|
36
|
+
profile = "black"
|
|
37
|
+
multi_line_output = 3
|
|
38
|
+
line_length = 88
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
python_version = "3.13"
|
|
42
|
+
warn_return_any = true
|
|
43
|
+
warn_unused_configs = true
|
|
44
|
+
disallow_untyped_defs = true
|
|
45
|
+
check_untyped_defs = true
|
|
46
|
+
strict = true
|
|
47
|
+
disallow_untyped_decorators = false
|
|
48
|
+
ignore_missing_imports = true
|
|
49
|
+
disable_error_code = ["misc"]
|
|
50
|
+
exclude = ["tests/.*"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 88
|
|
54
|
+
target-version = "py312"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = [
|
|
58
|
+
"E", # pycodestyle errors
|
|
59
|
+
"W", # pycodestyle warnings
|
|
60
|
+
"F", # pyflakes
|
|
61
|
+
"I", # isort
|
|
62
|
+
"C", # flake8-comprehensions
|
|
63
|
+
"B", # flake8-bugbear
|
|
64
|
+
"UP" # pyupgrade
|
|
65
|
+
]
|
|
66
|
+
ignore = [
|
|
67
|
+
"E203", # See https://github.com/psf/black/issues/315
|
|
68
|
+
"E501" # Line too long (handled by black)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.per-file-ignores]
|
|
72
|
+
"__init__.py" = ["F401"] # Ignore unused imports in __init__ files
|