roboherd 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

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.

Potentially problematic release.


This version of roboherd might be problematic. Click here for more details.

roboherd/__main__.py CHANGED
@@ -10,6 +10,7 @@ from roboherd.herd import RoboHerd
10
10
  from roboherd.herd.manager import HerdManager
11
11
  from roboherd.util import create_connection
12
12
  from roboherd.register import register as run_register
13
+ from roboherd.validators import validators
13
14
 
14
15
  logging.basicConfig(level=logging.INFO)
15
16
 
@@ -32,6 +33,7 @@ def main(ctx, connection_string, base_url, config_file):
32
33
  settings = dynaconf.Dynaconf(
33
34
  settings_files=[config_file],
34
35
  envvar_prefix="ROBOHERD",
36
+ validators=validators,
35
37
  )
36
38
  ctx.ensure_object(dict)
37
39
 
@@ -41,12 +43,12 @@ def main(ctx, connection_string, base_url, config_file):
41
43
  if connection_string:
42
44
  ctx.obj["connection_string"] = connection_string
43
45
  else:
44
- ctx.obj["connection_string"] = settings.get("connection_string")
46
+ ctx.obj["connection_string"] = settings.connection_string
45
47
 
46
48
  if base_url:
47
49
  ctx.obj["base_url"] = base_url
48
50
  else:
49
- ctx.obj["base_url"] = settings.get("base_url")
51
+ ctx.obj["base_url"] = settings.base_url
50
52
 
51
53
 
52
54
  @main.command()
@@ -5,11 +5,11 @@ from almabtrieb import Almabtrieb
5
5
 
6
6
 
7
7
  def get_raw(data: dict) -> dict:
8
- return data.get("data").get("raw")
8
+ return data.get("data", {}).get("raw")
9
9
 
10
10
 
11
11
  def get_parsed(data: dict) -> dict:
12
- result = data.get("data").get("parsed")
12
+ result = data.get("data", {}).get("parsed")
13
13
  if result is None:
14
14
  raise ValueError("No parsed data found")
15
15
  return result
@@ -23,11 +23,17 @@ ParsedData = Annotated[dict, Depends(get_parsed)]
23
23
 
24
24
 
25
25
  def get_activity(parsed: ParsedData) -> dict:
26
- return parsed.get("activity")
26
+ result = parsed.get("activity")
27
+ if not result:
28
+ raise ValueError("No activity found")
29
+ return result
27
30
 
28
31
 
29
32
  def get_embedded_object(parsed: ParsedData) -> dict:
30
- return parsed.get("embeddedObject")
33
+ result = parsed.get("embeddedObject")
34
+ if not result:
35
+ raise ValueError("No embedded object found")
36
+ return result
31
37
 
32
38
 
33
39
  Activity = Annotated[dict, Depends(get_activity)]
@@ -37,6 +43,7 @@ EmbeddedObject = Annotated[dict, Depends(get_embedded_object)]
37
43
  """The embedded object in the activity as parsed by muck_out"""
38
44
 
39
45
  Publisher = Callable[[dict], Awaitable[None]]
46
+ """Type returned by the publishing functions"""
40
47
 
41
48
 
42
49
  def construct_publish_object(connection: Almabtrieb, actor_id: str) -> Publisher:
@@ -46,5 +53,16 @@ def construct_publish_object(connection: Almabtrieb, actor_id: str) -> Publisher
46
53
  return publish
47
54
 
48
55
 
56
+ def construct_publish_activity(connection: Almabtrieb, actor_id: str) -> Publisher:
57
+ async def publish(data: dict):
58
+ await connection.trigger("publish_activity", {"actor": actor_id, "data": data})
59
+
60
+ return publish
61
+
62
+
49
63
  PublishObject = Annotated[Publisher, Depends(construct_publish_object)]
50
64
  """Allows one to publish an object as the actor. Assumes cattle_grid has the extension `simple_object_storage` or equivalent"""
65
+
66
+
67
+ PublishActivity = Annotated[Publisher, Depends(construct_publish_activity)]
68
+ """Allows one to publish an activity as the actor. Assumes cattle_grid has the extension `simple_object_storage` or equivalent"""
@@ -7,19 +7,19 @@ from .common import Profile
7
7
  try:
8
8
  from bovine.activitystreams import factories_for_actor_object
9
9
  from bovine.activitystreams.activity_factory import (
10
- ActivityFactory as BovineActivityFactory,
10
+ ActivityFactory as BovineActivityFactory, # type: ignore
11
11
  )
12
12
  from bovine.activitystreams.object_factory import (
13
- ObjectFactory as BovineObjectFactory,
13
+ ObjectFactory as BovineObjectFactory, # type: ignore
14
14
  )
15
15
 
16
- def get_activity_factory(profile: Profile) -> BovineActivityFactory:
16
+ def get_activity_factory(profile: Profile) -> BovineActivityFactory: # type: ignore
17
17
  activity_factory, _ = factories_for_actor_object(profile)
18
- return activity_factory
18
+ return activity_factory # type: ignore
19
19
 
20
- def get_object_factory(profile: Profile) -> BovineObjectFactory:
20
+ def get_object_factory(profile: Profile) -> BovineObjectFactory: # type: ignore
21
21
  _, object_factory = factories_for_actor_object(profile)
22
- return object_factory
22
+ return object_factory # type: ignore
23
23
 
24
24
  except ImportError:
25
25
 
@@ -34,8 +34,8 @@ except ImportError:
34
34
  raise ImportError("bovine not installed")
35
35
 
36
36
 
37
- ActivityFactory = Annotated[BovineActivityFactory, Depends(get_activity_factory)]
37
+ ActivityFactory = Annotated[BovineActivityFactory, Depends(get_activity_factory)] # type: ignore
38
38
  """The activity factory of type [bovine.activitystreams.activity_factory.ActivityFactory][]"""
39
39
 
40
- ObjectFactory = Annotated[BovineObjectFactory, Depends(get_object_factory)]
40
+ ObjectFactory = Annotated[BovineObjectFactory, Depends(get_object_factory)] # type: ignore
41
41
  """The object factory of type [bovine.activitystreams.object_factory.ObjectFactory][]"""
@@ -5,7 +5,9 @@ from roboherd.cow import RoboCow
5
5
 
6
6
 
7
7
  def get_profile(cow: RoboCow) -> dict:
8
- return cow.profile
8
+ if cow.internals.profile is None:
9
+ raise ValueError("Cow has no profile")
10
+ return cow.internals.profile
9
11
 
10
12
 
11
13
  Profile = Annotated[dict, Depends(get_profile)]
roboherd/cow/__init__.py CHANGED
@@ -25,17 +25,8 @@ class CronEntry:
25
25
 
26
26
 
27
27
  @dataclass
28
- class RoboCow:
29
- information: Information = field(
30
- metadata=dict(description="Information about the cow")
31
- )
32
-
33
- auto_follow: bool = field(
34
- default=True,
35
- metadata=dict(
36
- description="""Whether to automatically accept follow requests"""
37
- ),
38
- )
28
+ class RoboCowInternals:
29
+ """Internal data for the cow"""
39
30
 
40
31
  profile: dict | None = field(
41
32
  default=None,
@@ -69,6 +60,27 @@ class RoboCow:
69
60
 
70
61
  startup_routine: Callable | None = None
71
62
 
63
+ base_url: str | None = field(default=None)
64
+
65
+
66
+ @dataclass
67
+ class RoboCow:
68
+ information: Information = field(
69
+ metadata=dict(description="Information about the cow")
70
+ )
71
+
72
+ auto_follow: bool = field(
73
+ default=True,
74
+ metadata=dict(
75
+ description="""Whether to automatically accept follow requests"""
76
+ ),
77
+ )
78
+
79
+ internals: RoboCowInternals = field(
80
+ default_factory=RoboCowInternals,
81
+ metadata=dict(description="Internal data for the cow"),
82
+ )
83
+
72
84
  def action(self, action: str = "*", activity_type: str = "*"):
73
85
  """Adds a handler for an event. Use "*" as a wildcard.
74
86
 
@@ -90,15 +102,15 @@ class RoboCow:
90
102
 
91
103
  def inner(func):
92
104
  config.func = func
93
- self.handlers.add_handler(config, func)
94
- self.handler_configuration.append(config)
105
+ self.internals.handlers.add_handler(config, func)
106
+ self.internals.handler_configuration.append(config)
95
107
  return func
96
108
 
97
109
  return inner
98
110
 
99
111
  def cron(self, crontab):
100
112
  def inner(func):
101
- self.cron_entries.append(CronEntry(crontab, func))
113
+ self.internals.cron_entries.append(CronEntry(crontab, func))
102
114
 
103
115
  return func
104
116
 
@@ -119,7 +131,7 @@ class RoboCow:
119
131
  action="incoming",
120
132
  activity_type="*",
121
133
  )
122
- self.handlers.add_handler(config, func)
134
+ self.internals.handlers.add_handler(config, func)
123
135
  return func
124
136
 
125
137
  def incoming_create(self, func):
@@ -137,75 +149,57 @@ class RoboCow:
137
149
  config = HandlerConfiguration(
138
150
  action="incoming", activity_type="Create", func=func
139
151
  )
140
- self.handler_configuration.append(config)
141
- self.handlers.add_handler(config, func)
152
+ self.internals.handler_configuration.append(config)
153
+ self.internals.handlers.add_handler(config, func)
142
154
  return func
143
155
 
144
156
  def startup(self, func):
145
157
  """Adds a startup routine to be run when the cow is started."""
146
158
 
147
- self.startup_routine = func
159
+ self.internals.startup_routine = func
148
160
 
149
161
  def needs_update(self):
150
162
  """Checks if the cow needs to be updated"""
151
- if self.profile is None:
163
+ if self.internals.profile is None:
152
164
  return True
153
165
 
154
- if self.information.name != self.profile.get("name"):
166
+ if self.information.name != self.internals.profile.get("name"):
155
167
  return True
156
168
 
157
- if self.information.description != self.profile.get("summary"):
169
+ if self.information.description != self.internals.profile.get("summary"):
158
170
  return True
159
171
 
160
172
  return False
161
173
 
162
- def update_data(self):
163
- """
164
- Returns the update_actor message to send to cattle_grid
165
-
166
- ```pycon
167
- >>> info = Information(handle="moocow", name="name", description="description")
168
- >>> cow = RoboCow(information=info, actor_id="http://host.example/actor/1")
169
- >>> cow.update_data()
170
- {'actor': 'http://host.example/actor/1',
171
- 'profile': {'name': 'name',
172
- 'summary': 'description'},
173
- 'automaticallyUpdateFollowers': True}
174
-
175
- ```
176
- """
177
- return {
178
- "actor": self.actor_id,
179
- "profile": {
180
- "name": self.information.name,
181
- "summary": self.information.description,
182
- },
183
- "automaticallyUpdateFollowers": self.auto_follow,
184
- }
185
-
186
174
  async def run_startup(self, connection: Almabtrieb):
187
175
  """Runs when the cow is birthed"""
188
176
 
189
- if self.profile is None:
190
- result = await connection.fetch(self.actor_id, self.actor_id)
191
- self.profile = result.data
177
+ if self.internals.profile is None:
178
+ if not self.internals.actor_id:
179
+ raise ValueError("Actor ID is not set")
180
+ result = await connection.fetch(
181
+ self.internals.actor_id, self.internals.actor_id
182
+ )
183
+ if not result.data:
184
+ raise ValueError("Could not retrieve profile")
185
+ self.internals.profile = result.data
192
186
 
193
- if self.cron_entries:
187
+ if self.internals.cron_entries:
194
188
  frequency = ", ".join(
195
- get_description(entry.crontab) for entry in self.cron_entries
189
+ get_description(entry.crontab) for entry in self.internals.cron_entries
196
190
  )
197
191
  self.information.frequency = frequency
198
192
 
199
- update = determine_profile_update(self.information, self.profile)
193
+ update = determine_profile_update(self.information, self.internals.profile)
200
194
 
201
195
  if update:
202
196
  logger.info("Updating profile for %s", self.information.handle)
203
197
 
204
198
  await connection.trigger("update_actor", update)
205
199
 
206
- if self.startup_routine:
207
- await inject(self.startup_routine)(
208
- cow=self,
209
- connection=connection,
210
- actor_id=self.actor_id,
211
- )
200
+ if self.internals.startup_routine:
201
+ await inject(self.internals.startup_routine)(
202
+ cow=self, # type:ignore
203
+ connection=connection, # type:ignore
204
+ actor_id=self.internals.actor_id, # type:ignore
205
+ ) # type:ignore
roboherd/cow/const.py ADDED
@@ -0,0 +1,6 @@
1
+ default_icon = {
2
+ "mediaType": "image/png",
3
+ "type": "Image",
4
+ "url": "https://dev.bovine.social/assets/bull-horns.png",
5
+ }
6
+ """The default icon to be used"""
roboherd/cow/profile.py CHANGED
@@ -1,5 +1,15 @@
1
1
  from urllib.parse import urlparse
2
- from bovine.activitystreams.utils import as_list
2
+
3
+ try:
4
+ from bovine.activitystreams.utils import as_list
5
+ except ImportError:
6
+
7
+ def as_list(value):
8
+ if isinstance(value, list):
9
+ return value
10
+
11
+ return [value]
12
+
3
13
 
4
14
  from .types import Information
5
15
 
@@ -14,6 +24,9 @@ def profile_part_needs_update(information: Information, profile: dict) -> bool:
14
24
  if information.type != profile.get("type"):
15
25
  return True
16
26
 
27
+ if information.icon != profile.get("icon"):
28
+ return True
29
+
17
30
  return False
18
31
 
19
32
 
@@ -27,7 +40,7 @@ def key_index_from_attachment(attachments: list[dict], key: str) -> int | None:
27
40
 
28
41
 
29
42
  def determine_action_for_key_and_value(
30
- attachments: list[dict], key: str, value: str
43
+ attachments: list[dict], key: str, value: str | None
31
44
  ) -> dict | None:
32
45
  idx = key_index_from_attachment(attachments, key)
33
46
  if idx is None:
@@ -78,19 +91,19 @@ def determine_actions(information: Information, profile: dict) -> list[dict] | N
78
91
  "identifier": "acct:"
79
92
  + information.handle
80
93
  + "@"
81
- + urlparse(profile.get("id")).netloc,
94
+ + str(urlparse(profile.get("id")).netloc),
82
95
  "primary": True,
83
96
  }
84
97
  )
85
98
 
86
- actions = list(filter(lambda x: x, actions))
99
+ actions = [x for x in actions if x is not None]
87
100
 
88
101
  if len(actions) == 0:
89
102
  return None
90
103
  return actions
91
104
 
92
105
 
93
- def determine_profile_update(information: Information, profile: dict) -> dict:
106
+ def determine_profile_update(information: Information, profile: dict) -> dict | None:
94
107
  """Returns the update for the profile"""
95
108
 
96
109
  update = {"actor": profile.get("id")}
@@ -100,6 +113,7 @@ def determine_profile_update(information: Information, profile: dict) -> dict:
100
113
  "type": information.type,
101
114
  "name": information.name,
102
115
  "summary": information.description,
116
+ "icon": information.icon,
103
117
  }
104
118
 
105
119
  actions = determine_actions(information, profile)
roboherd/cow/test_init.py CHANGED
@@ -27,7 +27,8 @@ def test_needs_update(name, summary, profile, expected):
27
27
  name=name,
28
28
  description=summary,
29
29
  )
30
- cow = RoboCow(information=info, profile=profile)
30
+ cow = RoboCow(information=info)
31
+ cow.internals.profile = profile
31
32
 
32
33
  assert cow.needs_update() == expected
33
34
 
@@ -40,13 +41,13 @@ def test_cron():
40
41
  async def test_func():
41
42
  pass
42
43
 
43
- assert len(cow.cron_entries) == 1
44
+ assert len(cow.internals.cron_entries) == 1
44
45
 
45
46
 
46
47
  async def test_startup():
47
48
  info = Information(handle="testcow")
48
49
  cow = RoboCow(information=info)
49
- cow.profile = {"id": "http://host.test/actor/cow"}
50
+ cow.internals.profile = {"id": "http://host.test/actor/cow"}
50
51
  mock = AsyncMock()
51
52
 
52
53
  cow.startup(mock)
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
 
3
3
  from .types import Information, MetaInformation
4
-
4
+ from .const import default_icon
5
5
  from .profile import determine_profile_update
6
6
 
7
7
 
@@ -14,6 +14,7 @@ from .profile import determine_profile_update
14
14
  ],
15
15
  )
16
16
  def test_determine_profile_update_no_update(info_params, profile):
17
+ profile["icon"] = default_icon
17
18
  info = Information(**info_params)
18
19
 
19
20
  assert determine_profile_update(info, profile) is None
@@ -27,13 +28,22 @@ def test_determine_profile_update():
27
28
 
28
29
  assert result == {
29
30
  "actor": "http://host.test/actor/1",
30
- "profile": {"type": "Service", "name": "name", "summary": "description"},
31
+ "profile": {
32
+ "type": "Service",
33
+ "name": "name",
34
+ "summary": "description",
35
+ "icon": default_icon,
36
+ },
31
37
  }
32
38
 
33
39
 
34
40
  def test_determine_profile_update_author():
35
41
  info = Information(meta_information=MetaInformation(author="acct:author@host.test"))
36
- profile = {"id": "http://host.test/actor/1", "type": "Service"}
42
+ profile = {
43
+ "id": "http://host.test/actor/1",
44
+ "type": "Service",
45
+ "icon": default_icon,
46
+ }
37
47
 
38
48
  result = determine_profile_update(info, profile)
39
49
 
@@ -47,3 +57,25 @@ def test_determine_profile_update_author():
47
57
  }
48
58
  ],
49
59
  }
60
+
61
+
62
+ def test_profile_update_for_new_preferredUsername():
63
+ info = Information(handle="handle")
64
+ profile = {
65
+ "id": "http://host.test/actor/1",
66
+ "type": "Service",
67
+ "icon": default_icon,
68
+ }
69
+
70
+ result = determine_profile_update(info, profile)
71
+
72
+ assert result == {
73
+ "actor": "http://host.test/actor/1",
74
+ "actions": [
75
+ {
76
+ "action": "add_identifier",
77
+ "identifier": "acct:handle@host.test",
78
+ "primary": True,
79
+ }
80
+ ],
81
+ }
roboherd/cow/types.py CHANGED
@@ -1,18 +1,20 @@
1
1
  from pydantic import BaseModel, Field
2
2
 
3
+ from .const import default_icon
4
+
3
5
 
4
6
  class MetaInformation(BaseModel):
5
7
  """Meta Information about the bot. This includes
6
8
  information such as the author and the source repository"""
7
9
 
8
10
  source: str | None = Field(
9
- None,
11
+ default=None,
10
12
  examples=["https://forge.example/repo"],
11
13
  description="The source repository",
12
14
  )
13
15
 
14
16
  author: str | None = Field(
15
- None,
17
+ default=None,
16
18
  examples=["acct:author@domain.example"],
17
19
  description="The author, often a Fediverse handle",
18
20
  )
@@ -22,23 +24,25 @@ class Information(BaseModel):
22
24
  """Information about the cow"""
23
25
 
24
26
  type: str = Field(
25
- "Service", examples=["Service"], description="ActivityPub type of the actor."
27
+ default="Service",
28
+ examples=["Service"],
29
+ description="ActivityPub type of the actor.",
26
30
  )
27
31
 
28
32
  handle: str | None = Field(
29
- None,
33
+ default=None,
30
34
  examples=["moocow"],
31
35
  description="Used as the handle in `acct:handle@domain.example`",
32
36
  )
33
37
 
34
38
  name: str | None = Field(
35
- None,
39
+ default=None,
36
40
  examples=["The mooing cow 🐮"],
37
41
  description="The display name of the cow",
38
42
  )
39
43
 
40
44
  description: str | None = Field(
41
- None,
45
+ default=None,
42
46
  examples=[
43
47
  "I'm a cow that moos.",
44
48
  """<p>An example bot to illustrate Roboherd</p><p>For more information on RoboHerd, see <a href="https://codeberg.org/bovine/roboherd">its repository</a>.</p>""",
@@ -46,13 +50,18 @@ class Information(BaseModel):
46
50
  description="The description of the cow, used as summary of the actor",
47
51
  )
48
52
 
53
+ icon: dict = Field(
54
+ default=default_icon,
55
+ description="The profile image",
56
+ )
57
+
49
58
  frequency: str | None = Field(
50
- None,
59
+ default=None,
51
60
  examples=["daily"],
52
61
  description="Frequency of posting. Is set automatically if cron expressions are used.",
53
62
  )
54
63
 
55
64
  meta_information: MetaInformation = Field(
56
- MetaInformation(),
65
+ default=MetaInformation(),
57
66
  description="Meta information about the cow, such as the source repository",
58
67
  )
roboherd/cow/util.py CHANGED
@@ -19,5 +19,8 @@ async def call_handler(
19
19
  cow=None,
20
20
  ):
21
21
  return await inject(handler_info.func)(
22
- data=data, connection=connection, actor_id=actor_id, cow=cow
22
+ data=data, # type: ignore
23
+ connection=connection,
24
+ actor_id=actor_id,
25
+ cow=cow,
23
26
  )
@@ -45,7 +45,7 @@ async def create(
45
45
  raw: RawData, publish_object: PublishObject, object_factory: ObjectFactory
46
46
  ):
47
47
  note = (
48
- object_factory.reply(
48
+ object_factory.reply( # type: ignore
49
49
  raw.get("object"),
50
50
  content=reply_content(raw),
51
51
  )
@@ -21,7 +21,9 @@ See <a href="https://codeberg.org/helge/roboherd">codeberg.org</a>.""",
21
21
 
22
22
 
23
23
  @moocow.incoming_create
24
- async def on_incoming_create(obj: EmbeddedObject, publisher: PublishObject):
24
+ async def on_incoming_create(
25
+ obj: EmbeddedObject, publisher: PublishObject, actor_id: str
26
+ ):
25
27
  recipient = obj.get("attributedTo")
26
28
 
27
29
  logger.info("Replying to %s", recipient)
@@ -29,7 +31,7 @@ async def on_incoming_create(obj: EmbeddedObject, publisher: PublishObject):
29
31
  obj = {
30
32
  "@context": "https://www.w3.org/ns/activitystreams",
31
33
  "type": "Note",
32
- "attributedTo": moocow.actor_id,
34
+ "attributedTo": actor_id,
33
35
  "to": [recipient],
34
36
  "cc": ["https://www.w3.org/ns/activitystreams#Public"],
35
37
  "content": "moo",
@@ -51,7 +51,7 @@ async def post_number(
51
51
  ):
52
52
  number = random.randint(0, 1000)
53
53
 
54
- note = factory.note(content=f"Number: {number}").as_public().build()
54
+ note = factory.note(content=f"Number: {number}").as_public().build() # type: ignore
55
55
  await publisher(note)
56
56
 
57
57
  handle = "even" if number % 2 == 0 else "odd"
@@ -18,5 +18,5 @@ bot = RoboCow(
18
18
  @bot.cron("42 * * * *")
19
19
  async def crow(publisher: PublishObject, object_factory: ObjectFactory):
20
20
  await publisher(
21
- object_factory.note(content="cock-a-doodle-doo").as_public().build()
21
+ object_factory.note(content="cock-a-doodle-doo").as_public().build() # type: ignore
22
22
  )
@@ -18,4 +18,5 @@ bot = RoboCow(
18
18
 
19
19
  @bot.startup
20
20
  async def startup(publish_object: PublishObject, object_factory: ObjectFactory):
21
- await publish_object(object_factory.note(content="Booo! 🐦").as_public().build())
21
+ note = object_factory.note(content="Booo! 🐦").as_public().build() # type: ignore
22
+ await publish_object(note)
roboherd/herd/__init__.py CHANGED
@@ -29,6 +29,9 @@ class RoboHerd:
29
29
  await self.process(connection)
30
30
 
31
31
  async def startup(self, connection: Almabtrieb):
32
+ if not connection.information:
33
+ raise Exception("Could not get information from server")
34
+
32
35
  self.cows = self.manager.existing_cows(connection.information.actors)
33
36
 
34
37
  cows_to_create = self.manager.cows_to_create(connection.information.actors)
@@ -38,12 +41,12 @@ class RoboHerd:
38
41
  cow = cow_config.load()
39
42
  result = await connection.create_actor(
40
43
  name=f"{self.manager.prefix}{cow_config.name}",
41
- base_url=self.base_url,
44
+ base_url=cow.internals.base_url or self.base_url,
42
45
  preferred_username=cow.information.handle,
43
46
  profile={"type": "Service"},
44
47
  automatically_accept_followers=True,
45
48
  )
46
- cow.actor_id = result.get("id")
49
+ cow.internals.actor_id = result.get("id")
47
50
 
48
51
  self.cows.append(cow)
49
52
 
@@ -60,9 +63,6 @@ class RoboHerd:
60
63
  scheduler = HerdScheduler(self.cron_entries(), connection)
61
64
  scheduler.create_task(tg)
62
65
 
63
- def introduce(self, cow: RoboCow):
64
- self.manager.add_to_herd(cow)
65
-
66
66
  def validate(self, connection):
67
67
  result = connection.information
68
68
 
@@ -81,7 +81,7 @@ class RoboHerd:
81
81
 
82
82
  result = []
83
83
  for cow in self.cows:
84
- for cron_entry in cow.cron_entries:
84
+ for cron_entry in cow.internals.cron_entries:
85
85
  result.append((cow, cron_entry))
86
86
 
87
87
  return result
@@ -89,6 +89,6 @@ class RoboHerd:
89
89
  def incoming_handlers(self) -> List[RoboCow]:
90
90
  result = []
91
91
  for cow in self.cows:
92
- if cow.handlers.has_handlers:
92
+ if cow.internals.handlers.has_handlers:
93
93
  result.append(cow)
94
94
  return result
@@ -30,7 +30,7 @@ class HerdManager:
30
30
  cow_config = self.herd_config.for_name(cow_name)
31
31
  if cow_config:
32
32
  cow = cow_config.load()
33
- cow.actor_id = info.id
33
+ cow.internals.actor_id = info.id
34
34
  existing_cows.append(cow)
35
35
 
36
36
  return existing_cows
@@ -43,4 +43,6 @@ class HerdManager:
43
43
  }
44
44
  names_to_create = self.herd_config.names - existing_names
45
45
 
46
- return {self.herd_config.for_name(name) for name in names_to_create}
46
+ cows = {self.herd_config.for_name(name) for name in names_to_create}
47
+
48
+ return {cow for cow in cows if cow}
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass, field
2
2
 
3
3
  from roboherd.cow import RoboCow
4
- from roboherd.util import load_cow
4
+ from .load import load_cow
5
5
 
6
6
 
7
7
  @dataclass
@@ -9,19 +9,26 @@ class CowConfig:
9
9
  name: str = field(metadata={"description": "Name of the cow, must be unique"})
10
10
  module: str
11
11
  attribute: str
12
+ config: dict
12
13
 
13
14
  @staticmethod
14
15
  def from_name_and_dict(name, cow: dict) -> "CowConfig":
15
16
  module, attribute = cow["bot"].split(":")
16
17
 
17
- return CowConfig(
18
- name=name,
19
- module=module,
20
- attribute=attribute,
21
- )
18
+ return CowConfig(name=name, module=module, attribute=attribute, config=cow)
22
19
 
23
20
  def load(self) -> RoboCow:
24
- return load_cow(self.module, self.attribute)
21
+ cow = load_cow(self.module, self.attribute)
22
+
23
+ if "name" in self.config:
24
+ cow.information.name = self.config["name"]
25
+ if "handle" in self.config:
26
+ cow.information.handle = self.config["handle"]
27
+
28
+ if "base_url" in self.config:
29
+ cow.internals.base_url = self.config["base_url"]
30
+
31
+ return cow
25
32
 
26
33
  def __hash__(self):
27
34
  return hash(self.name)
@@ -0,0 +1,15 @@
1
+ import copy
2
+ import importlib
3
+ from importlib import import_module
4
+
5
+ from roboherd.cow import RoboCow
6
+
7
+
8
+ def load_cow(module_name: str, attribute: str) -> RoboCow:
9
+ """Loads a cow from module name and attribute"""
10
+ module = import_module(module_name)
11
+ importlib.reload(module)
12
+
13
+ cow = getattr(module, attribute)
14
+
15
+ return copy.deepcopy(cow)
@@ -21,7 +21,7 @@ def test_config():
21
21
  },
22
22
  },
23
23
  }
24
- )
24
+ ) # type: ignore
25
25
  return config
26
26
 
27
27
 
@@ -38,13 +38,29 @@ def test_from_name_and_dict():
38
38
  assert cow.attribute == "attribute"
39
39
 
40
40
 
41
+ def test_from_name_and_dict_with_new_name():
42
+ name = "cow"
43
+ value = {
44
+ "bot": "roboherd.examples.moocow:moocow",
45
+ "handle": "new_handle",
46
+ "name": "new name",
47
+ }
48
+
49
+ config = CowConfig.from_name_and_dict(name, value)
50
+
51
+ cow = config.load()
52
+
53
+ assert cow.information.name == "new name"
54
+ assert cow.information.handle == "new_handle"
55
+
56
+
41
57
  def test_load_config(test_config):
42
58
  herd = HerdConfig.from_settings(test_config)
43
59
 
44
60
  assert len(herd.cows) == 2
45
61
 
46
62
  moocow = herd.for_name("moocow")
47
-
63
+ assert moocow
48
64
  assert moocow.name == "moocow"
49
65
 
50
66
 
@@ -0,0 +1,19 @@
1
+ from roboherd.cow import RoboCow
2
+
3
+ from .load import load_cow
4
+
5
+
6
+ def test_load_cow():
7
+ cow = load_cow("roboherd.examples.moocow", "moocow")
8
+ assert isinstance(cow, RoboCow)
9
+
10
+
11
+ def test_load_cow_can_overwrite_variables():
12
+ one = load_cow("roboherd.examples.moocow", "moocow")
13
+
14
+ one.information.name = "A cow eating watermelons"
15
+
16
+ two = load_cow("roboherd.examples.moocow", "moocow")
17
+
18
+ assert two.information.name == "The mooing cow 🐮"
19
+ assert one.information.name == "A cow eating watermelons"
@@ -21,7 +21,7 @@ def test_config():
21
21
  },
22
22
  },
23
23
  }
24
- )
24
+ ) # type: ignore
25
25
  return config
26
26
 
27
27
 
@@ -26,7 +26,7 @@ class HerdProcessor:
26
26
  async def process_incoming(self, connection):
27
27
  actor_id_to_cow_map = {}
28
28
  for cow in self.incoming_handlers:
29
- actor_id_to_cow_map[cow.actor_id] = cow
29
+ actor_id_to_cow_map[cow.internals.actor_id] = cow
30
30
 
31
31
  async for msg in connection.incoming():
32
32
  actor_id = msg["actor"]
@@ -34,6 +34,6 @@ class HerdProcessor:
34
34
  cow = actor_id_to_cow_map.get(actor_id)
35
35
  logger.info(cow)
36
36
  if cow:
37
- await cow.handlers.handle(
37
+ await cow.internals.handlers.handle(
38
38
  msg, "incoming", connection, actor_id, cow=cow
39
39
  )
@@ -38,7 +38,7 @@ class HerdScheduler:
38
38
  inject(entry.func),
39
39
  trigger=trigger,
40
40
  kwargs={
41
- "actor_id": cow.actor_id,
41
+ "actor_id": cow.internals.actor_id,
42
42
  "connection": self.connection,
43
43
  "cow": cow,
44
44
  },
@@ -0,0 +1,10 @@
1
+ from dynaconf import Dynaconf
2
+
3
+ from .validators import validators
4
+
5
+
6
+ def test_validators():
7
+ settings = Dynaconf(validators=validators)
8
+
9
+ assert settings.base_url is None
10
+ assert settings.connection_string is None
roboherd/util.py CHANGED
@@ -1,13 +1,9 @@
1
1
  import click
2
2
  import logging
3
- import copy
4
- import importlib
5
- from importlib import import_module
6
- from urllib.parse import parse_qs, urlparse
7
3
 
4
+ from urllib.parse import urlparse
8
5
  from almabtrieb import Almabtrieb
9
6
 
10
- from roboherd.cow import RoboCow
11
7
 
12
8
  logger = logging.getLogger(__name__)
13
9
 
@@ -47,52 +43,6 @@ def parse_connection_string(connection_string: str) -> dict:
47
43
  }
48
44
 
49
45
 
50
- def load_cow(module_name: str, attribute: str) -> RoboCow:
51
- """Loads a cow from module name and attribute"""
52
- module = import_module(module_name)
53
- importlib.reload(module)
54
-
55
- cow = getattr(module, attribute)
56
-
57
- return copy.deepcopy(cow)
58
-
59
-
60
- def import_cow(name: str) -> RoboCow:
61
- """Imports a cow from a string of the form
62
- `module.name:attribute`. Here attribute should
63
- be of type [roboherd.cow.RoboCow][roboherd.cow.RoboCow].
64
-
65
- ```pycon
66
- >>> cow = import_cow("roboherd.examples.moocow:moocow")
67
- >>> cow.information.handle
68
- 'moocow'
69
-
70
- ```
71
- """
72
- try:
73
- query = None
74
- module_name, attribute = name.split(":")
75
- if "?" in attribute:
76
- attribute, query = attribute.split("?")
77
-
78
- cow = load_cow(module_name, attribute)
79
-
80
- if query:
81
- parsed_query = parse_qs(query)
82
- handle = parsed_query.get("handle", [None])[0]
83
- if handle:
84
- cow.information.handle = handle
85
-
86
- return cow
87
-
88
- except Exception as e:
89
- logger.error("Failed to import cow with name: %s", name)
90
- logger.error("names should have the form module:attribute")
91
- logger.exception(e)
92
-
93
- raise ImportError("Failed to load module")
94
-
95
-
96
46
  def create_connection(ctx):
97
47
  connection_string = ctx.obj["connection_string"]
98
48
  base_url = ctx.obj["base_url"]
roboherd/validators.py ADDED
@@ -0,0 +1,6 @@
1
+ from dynaconf import Validator
2
+
3
+ validators = [
4
+ Validator("base_url", default=None),
5
+ Validator("connection_string", default=None),
6
+ ]
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roboherd
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: A Fediverse bot framework
5
5
  Requires-Python: >=3.11
6
+ Requires-Dist: aiohttp>=3.11.12
6
7
  Requires-Dist: almabtrieb[mqtt]>=0.1.0a1
7
8
  Requires-Dist: apscheduler>=3.11.0
8
9
  Requires-Dist: click>=8.1.8
@@ -0,0 +1,44 @@
1
+ roboherd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ roboherd/__main__.py,sha256=cY4f-39QCDhStJ4gDuuSz46RDFE_AKnu08sTCpQEEV4,2933
3
+ roboherd/register.py,sha256=Gqa5aT2supVJMj7z21btMYRma3_WW0oK5gjZftr048s,976
4
+ roboherd/test_validators.py,sha256=UiFgJkutmXBLjGpdLP2IBYCEf75quwnRHX7Z51G8Xqo,222
5
+ roboherd/util.py,sha256=nrFRtyfvMQ8oXoGDYxmCAXcIN14dWBAPGv0cI_B_TP0,1710
6
+ roboherd/validators.py,sha256=2mc43ZGwFazp4f3B9J4RxZCU4Y_ErSNotib8MnYVtmY,140
7
+ roboherd/annotations/__init__.py,sha256=UhDSVQ4cj2iwIr5qn6dZMAyyY-A9KMJHnqhyVYsRYww,2095
8
+ roboherd/annotations/bovine.py,sha256=qXkliFVT63BfGn820Bopwd7O41ofP7VbNomxyit4zBg,1494
9
+ roboherd/annotations/common.py,sha256=DN1jt1P79pU7XnmiocwTQTTQnn3vJlSSkP8kiHGAbfs,334
10
+ roboherd/cow/__init__.py,sha256=MlMvjFbodwZf6vpKlPfIpSn5SHu9IugacSH5ylbySTY,5826
11
+ roboherd/cow/const.py,sha256=fj5nUJUIlcpr1AU2Ur55ToR7iVmYv4UnfdxiQwguv-k,166
12
+ roboherd/cow/handlers.py,sha256=k5Tc1M--wqmZ2EZvzIfID4dp8XE0rN18puMTKkNVjjE,1491
13
+ roboherd/cow/profile.py,sha256=ldeYq50lh97dl50QjOZiGopEbXtCoEsE5QJoXB5XUYU,3330
14
+ roboherd/cow/test_handlers.py,sha256=SwrZZcU_BD2BpJUFg18mFEsyUqu7N81-MkjIaGv7whQ,1673
15
+ roboherd/cow/test_init.py,sha256=bnK8IIbbeS1Y7vJaFg5HzCL6UoZeCl6mo_SAQqJAgQY,1409
16
+ roboherd/cow/test_profile.py,sha256=f7HE0iVgbpuNv6znEqi-4l1o_8UZ9ufQpjSVP7Xf1wc,2160
17
+ roboherd/cow/test_util.py,sha256=8FLRtVdSMmIo6NSpCpB9mS0PwOCpGgUeDAA1q6Gv0P4,430
18
+ roboherd/cow/types.py,sha256=TGXTcPuND7xMly3xFXZyIR7UE3XWyF_MLRuBHWKoFEE,1925
19
+ roboherd/cow/util.py,sha256=ASQn7AnSl5lskECqCyqOKn5BYC8yOces4FK3uNV8010,534
20
+ roboherd/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ roboherd/examples/dev_null.py,sha256=6SZ9wlcawSBqhdq6Zv4xPXH5eKH2g1VaRwv3hSCDucE,308
22
+ roboherd/examples/json_echo.py,sha256=EFqNwaSKs0hTQjKZVatjn6i3O7rQff2GTHnEfpL1ha0,1370
23
+ roboherd/examples/meta.py,sha256=tFhHe-DJn2J13Mm22f-gFm5I8_3uQwLU4mAkrK2xzEM,170
24
+ roboherd/examples/moocow.py,sha256=DMPuR8XBpRRHKXFZM-2_VLYLWx8Iz4OdcE0QTVtYSFs,1097
25
+ roboherd/examples/number.py,sha256=Sj9aYfdtzANUfWVJnoX73zEvDNVcdrr-STZDSwNRHlI,2003
26
+ roboherd/examples/rooster.py,sha256=5_rPGLQTLv1s0cyE6l7WXLum-QLpQZ2p63YBkfrxKFw,596
27
+ roboherd/examples/scarecrow.py,sha256=KGZP0e5WHkCNSnGSlW3BeymcRZ4PqaWjAs_q5IC-1fE,649
28
+ roboherd/herd/__init__.py,sha256=6jWgYnuJOpXtYS03mdjEgWd0zXv5kj9w9d7K6UsCPbc,3068
29
+ roboherd/herd/builder.py,sha256=MSVPRF0Jsxure9kdyCoYJHQ7nYilGAD0_uQaGQ-rQyE,619
30
+ roboherd/herd/processor.py,sha256=ncXsYfuTRTT_0-K453COF_oAiGBJN0u5eP8NoeZmWik,1042
31
+ roboherd/herd/scheduler.py,sha256=pbWxOo9pnjAoAJhbvaczmSghkp6Y8Zp2HgXZ5zoOnDA,1276
32
+ roboherd/herd/test_herd.py,sha256=sQkzGCWdFveLklhaOJUybtl7odO-QOSDdd-_gan1py8,845
33
+ roboherd/herd/test_scheduler.py,sha256=wLisqRMSl734P_rjbqMNH5WTQKepwihgr7ZC32nEj80,424
34
+ roboherd/herd/types.py,sha256=_EidQbglm0jpsKX1EsL6U2qm_J5wCPhwUi6Avac22Ow,210
35
+ roboherd/herd/manager/__init__.py,sha256=NqOJsp1CdAobjARJGmzvU1ceTW7j2bt0FdRbpM8iFUw,1464
36
+ roboherd/herd/manager/config.py,sha256=VDuxB7GMoZdgSM2i1iG1o-IjtyScsSnO--6PEM8tx9U,1591
37
+ roboherd/herd/manager/load.py,sha256=BoeBID2UGP--sIKwITABQkQv2lMc9Y8pyp7_nleu2bw,351
38
+ roboherd/herd/manager/test_config.py,sha256=CnoqQ_exrpQdBWVoGxSns3HpccqgmhH9mNJIPTCKKAs,1665
39
+ roboherd/herd/manager/test_load.py,sha256=zyu5LIChMfTnxu_tYK63-bSOHYn1K1zUlbDY5DkE3GY,514
40
+ roboherd/herd/manager/test_manager.py,sha256=9pSMaH7zmN-zagYCIBpQcV3Q0sBT7XZSCvsmLVC0rOI,1047
41
+ roboherd-0.1.5.dist-info/METADATA,sha256=TU89GQjIVilPWX8Lc_It5tNYKCPuoI0MXaWdN-t25CY,830
42
+ roboherd-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ roboherd-0.1.5.dist-info/entry_points.txt,sha256=WebdVUmh8Ot-FupKJY6Du8LuFbmezt9yoy2UICqV3bE,52
44
+ roboherd-0.1.5.dist-info/RECORD,,
roboherd/test_util.py DELETED
@@ -1,24 +0,0 @@
1
- import pytest
2
-
3
- from roboherd.cow import RoboCow
4
-
5
- from .util import import_cow
6
-
7
-
8
- def test_import_cow():
9
- cow = import_cow("roboherd.examples.moocow:moocow")
10
-
11
- assert isinstance(cow, RoboCow)
12
- assert cow.information.handle == "moocow"
13
-
14
-
15
- def test_import_cow_failed():
16
- with pytest.raises(ImportError):
17
- import_cow("robocow:nocow")
18
-
19
-
20
- def test_import_cow_with_handle():
21
- cow = import_cow("roboherd.examples.moocow:moocow?handle=horse")
22
-
23
- assert isinstance(cow, RoboCow)
24
- assert cow.information.handle == "horse"
@@ -1,40 +0,0 @@
1
- roboherd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- roboherd/__main__.py,sha256=y2Ge8zajHn4rB4SlunLzmH5MNMCxklrhTaDdMbujjq8,2873
3
- roboherd/register.py,sha256=Gqa5aT2supVJMj7z21btMYRma3_WW0oK5gjZftr048s,976
4
- roboherd/test_util.py,sha256=Yor_YgXtvK3WnZu6jr7DvNtTLdR9YWHCA5QufHo_w2s,535
5
- roboherd/util.py,sha256=23X_-CiJoW6xd36YhTzIkXoS3y_eN5ILJDCC0GnBhm0,3065
6
- roboherd/annotations/__init__.py,sha256=hdW1HypuV6duCYxEhR2a9eidgcrC5-oAM-hHyzVh10E,1425
7
- roboherd/annotations/bovine.py,sha256=3LnS19pJ6Z1pSB50IaTxDnvaPlU_4qp_lFyQjAtJ5P0,1366
8
- roboherd/annotations/common.py,sha256=xr8FanMMPA1DDnCdguY4DLG9DbqmziaZxNR4-ww2ewk,239
9
- roboherd/cow/__init__.py,sha256=j6a1i_kAndo71Ufoy1bZr1FC9othg-cz-6x9z5yQ75E,5866
10
- roboherd/cow/handlers.py,sha256=k5Tc1M--wqmZ2EZvzIfID4dp8XE0rN18puMTKkNVjjE,1491
11
- roboherd/cow/profile.py,sha256=XEFU8wJYVQxrlbAhUWZSDhfxBn1IXm_6f8YBlpHvUjM,3060
12
- roboherd/cow/test_handlers.py,sha256=SwrZZcU_BD2BpJUFg18mFEsyUqu7N81-MkjIaGv7whQ,1673
13
- roboherd/cow/test_init.py,sha256=Te-4Z8lPpMlSv99OkzPLMtB607NSOfdRD3EB-6_3n1A,1370
14
- roboherd/cow/test_profile.py,sha256=edWKVL8VmOb8XtE1OQmwDLuw4mcDtxMKBfA9Kbd71qU,1423
15
- roboherd/cow/test_util.py,sha256=8FLRtVdSMmIo6NSpCpB9mS0PwOCpGgUeDAA1q6Gv0P4,430
16
- roboherd/cow/types.py,sha256=GtDnvpiH0w-Ux6BkhjNOgRdZ9YOITPB16BZd51MzKfA,1709
17
- roboherd/cow/util.py,sha256=oBytl3cOhYgZCCnihM3wkMxG8xQZUlohVSxCaaHDsYc,493
18
- roboherd/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- roboherd/examples/dev_null.py,sha256=6SZ9wlcawSBqhdq6Zv4xPXH5eKH2g1VaRwv3hSCDucE,308
20
- roboherd/examples/json_echo.py,sha256=vOwbCX7apXh87RNFdwDWZXaIpJuJO5Tcsck_9qjRpqw,1354
21
- roboherd/examples/meta.py,sha256=tFhHe-DJn2J13Mm22f-gFm5I8_3uQwLU4mAkrK2xzEM,170
22
- roboherd/examples/moocow.py,sha256=OkceeaRqkjR31Nue8anHtL_DMqd7finLzs-gUaKsVBo,1083
23
- roboherd/examples/number.py,sha256=U71mcmLVqtOkl6vkudHKlJmxmLVCI11q1ekQOoQfTag,1987
24
- roboherd/examples/rooster.py,sha256=505MzfGM6iqQtffncLOA9Oj5aDeep-_oWS7yEzJbh4o,580
25
- roboherd/examples/scarecrow.py,sha256=o-54QN63LmxfufslmCDvx1ilWQ7KVMsUBtl1tPMbkEs,617
26
- roboherd/herd/__init__.py,sha256=DUvzuyB-JWD-awhNzseoMvluugeD02PZF-hJrGVNtJc,2981
27
- roboherd/herd/builder.py,sha256=MSVPRF0Jsxure9kdyCoYJHQ7nYilGAD0_uQaGQ-rQyE,619
28
- roboherd/herd/processor.py,sha256=NkROTAPs6ZoYW_0TSDnNkAfos4cTnaFgwtW3wOFSgQY,1022
29
- roboherd/herd/scheduler.py,sha256=fkR-74bFZ73DmlJje_dQSytxnFFLV5hCa067mXdwvXs,1266
30
- roboherd/herd/test_herd.py,sha256=sQkzGCWdFveLklhaOJUybtl7odO-QOSDdd-_gan1py8,845
31
- roboherd/herd/test_scheduler.py,sha256=wLisqRMSl734P_rjbqMNH5WTQKepwihgr7ZC32nEj80,424
32
- roboherd/herd/types.py,sha256=_EidQbglm0jpsKX1EsL6U2qm_J5wCPhwUi6Avac22Ow,210
33
- roboherd/herd/manager/__init__.py,sha256=n4QFJXQafHhOJZyDO-mAAWE_hoXcE9vYwFGOPqFOiJM,1409
34
- roboherd/herd/manager/config.py,sha256=qfcED9PfzKzDCWaYHefYj8AImcsOFs5daGpJwUrXlV4,1313
35
- roboherd/herd/manager/test_config.py,sha256=cSf6cqFFr5Hbubuc_tDHyDPbVnsKbRg12WUONn6RxIc,1260
36
- roboherd/herd/manager/test_manager.py,sha256=U6xLOry1K74hd_l8ZtO56v8h1TLNn-UYYfsZJuxuCDA,1031
37
- roboherd-0.1.3.dist-info/METADATA,sha256=dsglqXLDAENTeakmj_vUQu8_jWg7JkRKjdOwMeuKm3s,798
38
- roboherd-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
- roboherd-0.1.3.dist-info/entry_points.txt,sha256=WebdVUmh8Ot-FupKJY6Du8LuFbmezt9yoy2UICqV3bE,52
40
- roboherd-0.1.3.dist-info/RECORD,,