roboherd 0.1.7__py3-none-any.whl → 0.1.9__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
@@ -13,6 +13,7 @@ from roboherd.register import register as run_register
13
13
  from roboherd.validators import validators
14
14
 
15
15
  logging.basicConfig(level=logging.INFO)
16
+ logging.captureWarnings(True)
16
17
 
17
18
 
18
19
  @click.group()
roboherd/cow/__init__.py CHANGED
@@ -76,6 +76,13 @@ class RoboCow:
76
76
  ),
77
77
  )
78
78
 
79
+ skip_profile_update: bool = field(
80
+ default=False,
81
+ metadata=dict(
82
+ description="When set to True the profile is not updated automatically. Useful when managing a cow from multiple scripts."
83
+ ),
84
+ )
85
+
79
86
  internals: RoboCowInternals = field(
80
87
  default_factory=RoboCowInternals,
81
88
  metadata=dict(description="Internal data for the cow"),
@@ -166,19 +173,6 @@ class RoboCow:
166
173
 
167
174
  self.internals.startup_routine = func
168
175
 
169
- def needs_update(self):
170
- """Checks if the cow needs to be updated"""
171
- if self.internals.profile is None:
172
- return True
173
-
174
- if self.information.name != self.internals.profile.get("name"):
175
- return True
176
-
177
- if self.information.description != self.internals.profile.get("summary"):
178
- return True
179
-
180
- return False
181
-
182
176
  async def run_startup(self, connection: Almabtrieb):
183
177
  """Runs when the cow is birthed"""
184
178
 
@@ -198,12 +192,8 @@ class RoboCow:
198
192
  )
199
193
  self.information.frequency = frequency
200
194
 
201
- update = determine_profile_update(self.information, self.internals.profile)
202
-
203
- if update:
204
- logger.info("Updating profile for %s", self.information.handle)
205
-
206
- await connection.trigger("update_actor", update)
195
+ if not self.skip_profile_update:
196
+ await self._run_profile_update(connection)
207
197
 
208
198
  if self.internals.startup_routine:
209
199
  await inject(self.internals.startup_routine)(
@@ -211,3 +201,14 @@ class RoboCow:
211
201
  connection=connection, # type:ignore
212
202
  actor_id=self.internals.actor_id, # type:ignore
213
203
  ) # type:ignore
204
+
205
+ async def _run_profile_update(self, connection: Almabtrieb):
206
+ if self.internals.profile is None:
207
+ raise ValueError("Profile is not set")
208
+
209
+ update = determine_profile_update(self.information, self.internals.profile)
210
+
211
+ if update:
212
+ logger.info("Updating profile for %s", self.information.handle)
213
+
214
+ await connection.trigger("update_actor", update)
roboherd/cow/test_init.py CHANGED
@@ -1,38 +1,9 @@
1
- import pytest
2
1
  from unittest.mock import AsyncMock
3
2
 
4
3
  from . import RoboCow
5
4
  from .types import Information
6
5
 
7
6
 
8
- @pytest.mark.parametrize(
9
- "name,summary,profile,expected",
10
- [
11
- ("moocow", None, None, True),
12
- ("moocow", None, {"id": "123"}, True),
13
- ("moocow", None, {"id": "123", "name": "moocow"}, False),
14
- ("moocow", None, {"id": "123", "name": "other"}, True),
15
- ("moocow", "description", {"id": "123", "name": "moocow"}, True),
16
- (
17
- "moocow",
18
- "description",
19
- {"id": "123", "name": "moocow", "summary": "description"},
20
- False,
21
- ),
22
- ],
23
- )
24
- def test_needs_update(name, summary, profile, expected):
25
- info = Information(
26
- handle="testcow",
27
- name=name,
28
- description=summary,
29
- )
30
- cow = RoboCow(information=info)
31
- cow.internals.profile = profile
32
-
33
- assert cow.needs_update() == expected
34
-
35
-
36
7
  def test_cron():
37
8
  info = Information(handle="testcow")
38
9
  cow = RoboCow(information=info)
@@ -49,9 +20,22 @@ async def test_startup():
49
20
  cow = RoboCow(information=info)
50
21
  cow.internals.profile = {"id": "http://host.test/actor/cow"}
51
22
  mock = AsyncMock()
23
+ connection = AsyncMock()
52
24
 
53
25
  cow.startup(mock)
54
26
 
55
- await cow.run_startup(AsyncMock())
27
+ await cow.run_startup(connection=connection)
56
28
 
57
29
  mock.assert_called_once()
30
+ connection.trigger.assert_awaited_once()
31
+
32
+
33
+ async def test_skip_startup():
34
+ info = Information(handle="testcow")
35
+ cow = RoboCow(information=info, skip_profile_update=True)
36
+ cow.internals.profile = {"id": "http://host.test/actor/cow"}
37
+
38
+ connection = AsyncMock()
39
+ await cow.run_startup(connection=connection)
40
+
41
+ connection.trigger.assert_not_awaited()
@@ -5,6 +5,7 @@ from .meta import meta_information
5
5
  bot = RoboCow.create(
6
6
  handle="rooster",
7
7
  name="The crowing rooster 🐓",
8
+ description="I'm a rooster that crows at a set frequency.",
8
9
  meta_information=meta_information,
9
10
  )
10
11
 
roboherd/herd/__init__.py CHANGED
@@ -63,6 +63,8 @@ class RoboHerd:
63
63
  scheduler = HerdScheduler(self.cron_entries(), connection)
64
64
  scheduler.create_task(tg)
65
65
 
66
+ connection.add_on_disconnect(scheduler.stop)
67
+
66
68
  def validate(self, connection):
67
69
  result = connection.information
68
70
 
@@ -1,9 +1,32 @@
1
+ from pydantic import BaseModel, Field
2
+
1
3
  from dataclasses import dataclass, field
2
4
 
3
5
  from roboherd.cow import RoboCow
4
6
  from .load import load_cow
5
7
 
6
8
 
9
+ class ConfigOverrides(BaseModel):
10
+ """Values used in `roboherd.toml` to overide the default
11
+ values in the imported cow. This class is meant as a
12
+ reference, and not meant to be directly used."""
13
+
14
+ name: str | None = Field(
15
+ default=None, description="set to override the name", examples=["New name"]
16
+ )
17
+ handle: str | None = Field(
18
+ default=None, description="set to override the handle", examples=["new-handle"]
19
+ )
20
+ base_url: str | None = Field(
21
+ default=None,
22
+ description="set to override the base url",
23
+ examples=["https://other.example"],
24
+ )
25
+ skip_profile_update: bool | None = Field(
26
+ default=None, description="set to skip updating the profile", examples=[True]
27
+ )
28
+
29
+
7
30
  @dataclass
8
31
  class CowConfig:
9
32
  name: str = field(metadata={"description": "Name of the cow, must be unique"})
@@ -20,13 +43,14 @@ class CowConfig:
20
43
  def load(self) -> RoboCow:
21
44
  cow = load_cow(self.module, self.attribute)
22
45
 
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"]
46
+ overrides = ConfigOverrides(**self.config)
47
+
48
+ for value in ["name", "handle", "base_url"]:
49
+ if getattr(overrides, value):
50
+ setattr(cow.information, value, getattr(overrides, value))
27
51
 
28
- if "base_url" in self.config:
29
- cow.internals.base_url = self.config["base_url"]
52
+ if overrides.skip_profile_update:
53
+ cow.skip_profile_update = overrides.skip_profile_update
30
54
 
31
55
  return cow
32
56
 
@@ -44,6 +44,7 @@ def test_from_name_and_dict_with_new_name():
44
44
  "bot": "roboherd.examples.moocow:moocow",
45
45
  "handle": "new_handle",
46
46
  "name": "new name",
47
+ "skip_profile_update": True,
47
48
  }
48
49
 
49
50
  config = CowConfig.from_name_and_dict(name, value)
@@ -52,6 +53,7 @@ def test_from_name_and_dict_with_new_name():
52
53
 
53
54
  assert cow.information.name == "new name"
54
55
  assert cow.information.handle == "new_handle"
56
+ assert cow.skip_profile_update is True
55
57
 
56
58
 
57
59
  def test_load_config(test_config):
@@ -28,12 +28,15 @@ class HerdProcessor:
28
28
  for cow in self.incoming_handlers:
29
29
  actor_id_to_cow_map[cow.internals.actor_id] = cow
30
30
 
31
+ logger.info("Incoming processing started for %s cows", len(actor_id_to_cow_map))
32
+
31
33
  async for msg in connection.incoming():
32
34
  actor_id = msg["actor"]
33
35
 
34
36
  cow = actor_id_to_cow_map.get(actor_id)
35
- logger.info(cow)
36
37
  if cow:
37
38
  await cow.internals.handlers.handle(
38
39
  msg, "incoming", connection, actor_id, cow=cow
39
40
  )
41
+
42
+ logger.warning("Process incoming ended")
@@ -19,22 +19,30 @@ logger = logging.getLogger(__name__)
19
19
  class HerdScheduler:
20
20
  entries: List[Tuple[RoboCow, CronEntry]]
21
21
  connection: Almabtrieb
22
+ scheduler: AsyncIOScheduler | None = None
23
+ task: asyncio.Task | None = None
22
24
 
23
25
  def create_task(self, task_group: asyncio.TaskGroup):
24
26
  if len(self.entries) == 0:
25
27
  logger.info("No tasks to schedule")
26
28
  return
27
- task_group.create_task(self.run())
29
+ if self.task:
30
+ raise Exception("Task already running")
31
+
32
+ self.task = task_group.create_task(self.run())
28
33
 
29
34
  async def run(self):
30
35
  if len(self.entries) == 0:
31
36
  return
32
37
 
33
- scheduler = AsyncIOScheduler()
38
+ if self.scheduler:
39
+ raise Exception("Scheduler already exists")
40
+
41
+ self.scheduler = AsyncIOScheduler()
34
42
 
35
43
  for cow, entry in self.entries:
36
44
  trigger = CronTrigger.from_crontab(entry.crontab)
37
- scheduler.add_job(
45
+ self.scheduler.add_job(
38
46
  inject(entry.func),
39
47
  trigger=trigger,
40
48
  kwargs={
@@ -44,7 +52,14 @@ class HerdScheduler:
44
52
  },
45
53
  )
46
54
 
47
- scheduler.start()
55
+ self.scheduler.start()
48
56
 
49
57
  while True:
50
58
  await asyncio.sleep(60 * 60)
59
+
60
+ async def stop(self):
61
+ logger.warning("Stopping scheduler")
62
+ if self.scheduler:
63
+ self.scheduler.shutdown()
64
+ if self.task:
65
+ self.task.cancel()
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: roboherd
3
+ Version: 0.1.9
4
+ Summary: A Fediverse bot framework
5
+ Project-URL: Documentation, https://bovine.codeberg.page/roboherd/
6
+ Project-URL: Repository, https://codeberg.org/bovine/roboherd
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: aiohttp>=3.11.12
9
+ Requires-Dist: almabtrieb[mqtt]>=0.2
10
+ Requires-Dist: apscheduler>=3.11.0
11
+ Requires-Dist: click>=8.1.8
12
+ Requires-Dist: cron-descriptor>=1.4.5
13
+ Requires-Dist: dynaconf>=3.2.6
14
+ Requires-Dist: fast-depends>=2.4.12
15
+ Requires-Dist: tomli-w>=1.1.0
16
+ Requires-Dist: watchfiles>=1.0.4
17
+ Provides-Extra: bovine
18
+ Requires-Dist: bovine>=0.5.15; extra == 'bovine'
19
+ Requires-Dist: markdown>=3.7; extra == 'bovine'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Roboherd
23
+
24
+ Roboherd is a framework for building Fediverse bots
25
+ using the [Cattle Drive Protocol](https://bovine.codeberg.page/cattle_grid/cattle_drive/).
26
+
27
+ For more information, see the [documentation](https://bovine.codeberg.page/roboherd/) or the [repository](https://codeberg.org/bovine/roboherd/).
28
+
29
+ ## Developping with cattle_grid
30
+
31
+ In your catle_grid `config` directory add a roboherd user, e.g.
32
+ a file `testing.toml` with content
33
+
34
+ ```toml
35
+ [testing]
36
+ enable = true
37
+
38
+ [[testing.accounts]]
39
+ name = "herd"
40
+ password = "pass"
41
+ permissions = ["admin"]
42
+ ```
43
+
44
+ Configure roboherd via `roboherd.toml`, e.g.
45
+
46
+ ```toml
47
+ base_url = "http://abel"
48
+ connection_string = "ws://herd:pass@localhost:3000/ws/"
49
+
50
+ [cow.rooster]
51
+ bot = "roboherd.examples.rooster:bot"
52
+ ```
53
+
54
+ This will trigger a periodic message to cattle_grid.
55
+
56
+ ### nginx for cattle_grid
57
+
58
+ The nginx in the `cattle_grid` configuration should forward the path `/ws/` to
59
+ rabbitmq (supporting mqtt over websockets)
60
+
61
+ ```nginx
62
+ server {
63
+ listen 80;
64
+
65
+ location /ws/ {
66
+ proxy_pass http://rabbitmq:15675;
67
+ proxy_http_version 1.1;
68
+ proxy_set_header Upgrade $http_upgrade;
69
+ proxy_set_header Connection $connection_upgrade;
70
+ proxy_read_timeout 86400; # neccessary to avoid websocket timeout disconnect
71
+ proxy_send_timeout 86400; # neccessary to avoid websocket timeout disconnect
72
+ proxy_redirect off;
73
+ proxy_buffering off;
74
+ }
75
+ }
76
+ ```
77
+
78
+ similarly `nginx` should forward port 80 to 3000 (in the docker compose file).
@@ -1,5 +1,5 @@
1
1
  roboherd/__init__.py,sha256=E89YtYyL77iJDJJkHS4Pn0JdTbmkmQS7Cp_XPqpRgx8,533
2
- roboherd/__main__.py,sha256=cY4f-39QCDhStJ4gDuuSz46RDFE_AKnu08sTCpQEEV4,2933
2
+ roboherd/__main__.py,sha256=DMsxzSdO_tN0RazYcJicF1eZZzWT7NAYg3jXi7kEuU8,2963
3
3
  roboherd/register.py,sha256=Gqa5aT2supVJMj7z21btMYRma3_WW0oK5gjZftr048s,976
4
4
  roboherd/test_validators.py,sha256=UiFgJkutmXBLjGpdLP2IBYCEf75quwnRHX7Z51G8Xqo,222
5
5
  roboherd/util.py,sha256=nrFRtyfvMQ8oXoGDYxmCAXcIN14dWBAPGv0cI_B_TP0,1710
@@ -7,12 +7,12 @@ roboherd/validators.py,sha256=2mc43ZGwFazp4f3B9J4RxZCU4Y_ErSNotib8MnYVtmY,140
7
7
  roboherd/annotations/__init__.py,sha256=BrP5UWiVnh8FwmHc6wEY3G69BStidGUpF8uP4Ea5-dg,1140
8
8
  roboherd/annotations/bovine.py,sha256=cFLUSFzTulikNjZb1lLq5v4tGjL1i7_dR3H6wimegCg,2322
9
9
  roboherd/annotations/common.py,sha256=n5rm8WTgsg116RczKdzm_VoPg1mBk_IMf2V7l8UgWKQ,1351
10
- roboherd/cow/__init__.py,sha256=y9NrhXf5ivjfwWBYCqZ0VG1q0gXAYA3_ItfPYWf1qiw,6102
10
+ roboherd/cow/__init__.py,sha256=wSfYordSLmn3azxWkZs36h3ipbS6NdPh9kAWyyg0NIU,6223
11
11
  roboherd/cow/const.py,sha256=fj5nUJUIlcpr1AU2Ur55ToR7iVmYv4UnfdxiQwguv-k,166
12
12
  roboherd/cow/handlers.py,sha256=k5Tc1M--wqmZ2EZvzIfID4dp8XE0rN18puMTKkNVjjE,1491
13
13
  roboherd/cow/profile.py,sha256=ldeYq50lh97dl50QjOZiGopEbXtCoEsE5QJoXB5XUYU,3330
14
14
  roboherd/cow/test_handlers.py,sha256=SwrZZcU_BD2BpJUFg18mFEsyUqu7N81-MkjIaGv7whQ,1673
15
- roboherd/cow/test_init.py,sha256=bnK8IIbbeS1Y7vJaFg5HzCL6UoZeCl6mo_SAQqJAgQY,1409
15
+ roboherd/cow/test_init.py,sha256=GrOFyEKbvkddrUQM7B0RFelNLc_rmdlxU_8C-DjoUh8,999
16
16
  roboherd/cow/test_profile.py,sha256=f7HE0iVgbpuNv6znEqi-4l1o_8UZ9ufQpjSVP7Xf1wc,2160
17
17
  roboherd/cow/test_util.py,sha256=8FLRtVdSMmIo6NSpCpB9mS0PwOCpGgUeDAA1q6Gv0P4,430
18
18
  roboherd/cow/types.py,sha256=TGXTcPuND7xMly3xFXZyIR7UE3XWyF_MLRuBHWKoFEE,1925
@@ -23,22 +23,22 @@ roboherd/examples/json_echo.py,sha256=TB7sdshsTj7KKOxIc1DzkGeacA9JmIOhNOHwGzyRrV
23
23
  roboherd/examples/meta.py,sha256=tFhHe-DJn2J13Mm22f-gFm5I8_3uQwLU4mAkrK2xzEM,170
24
24
  roboherd/examples/moocow.py,sha256=2bZ8oZOpysuf7l3DP3zM4YiaALQHFbWpbP3h_Xlok6E,974
25
25
  roboherd/examples/number.py,sha256=c4zALxrKR5POuAUTmzAF4wHVgXMPfrP736N7dgn4xzo,1845
26
- roboherd/examples/rooster.py,sha256=E-bqm8AKy_EOzptaH6xrlc5eDE8o9SBhfdNqHpEhUb4,437
26
+ roboherd/examples/rooster.py,sha256=ItPBpxhz72agkzFf13nv6csYVUJCZRabtUaGyKwUKRg,501
27
27
  roboherd/examples/scarecrow.py,sha256=jUnBaDJ2w9LQNIJAAfA53oJyRvKPJoKHtvHbeW-Pex0,626
28
- roboherd/herd/__init__.py,sha256=6jWgYnuJOpXtYS03mdjEgWd0zXv5kj9w9d7K6UsCPbc,3068
28
+ roboherd/herd/__init__.py,sha256=AtmpzoIbJwX82cx-9GdYMYQ5guj2vWb1dVYNGc1PReo,3126
29
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
30
+ roboherd/herd/processor.py,sha256=4wZUp4xND9ZhxLIedUN7Yu3jxKWuuFqJent7wRiaAE0,1153
31
+ roboherd/herd/scheduler.py,sha256=4zAPJkWs1ZGHlwTweHdhYb5xrHnpiSTpTEQ64drA-oE,1735
32
32
  roboherd/herd/test_herd.py,sha256=sQkzGCWdFveLklhaOJUybtl7odO-QOSDdd-_gan1py8,845
33
33
  roboherd/herd/test_scheduler.py,sha256=wLisqRMSl734P_rjbqMNH5WTQKepwihgr7ZC32nEj80,424
34
34
  roboherd/herd/types.py,sha256=_EidQbglm0jpsKX1EsL6U2qm_J5wCPhwUi6Avac22Ow,210
35
35
  roboherd/herd/manager/__init__.py,sha256=NqOJsp1CdAobjARJGmzvU1ceTW7j2bt0FdRbpM8iFUw,1464
36
- roboherd/herd/manager/config.py,sha256=VDuxB7GMoZdgSM2i1iG1o-IjtyScsSnO--6PEM8tx9U,1591
36
+ roboherd/herd/manager/config.py,sha256=8m-313_wn_-b4ivPSoPyyTQh0ONNLcd-Zz3pgWh8p1c,2429
37
37
  roboherd/herd/manager/load.py,sha256=BoeBID2UGP--sIKwITABQkQv2lMc9Y8pyp7_nleu2bw,351
38
- roboherd/herd/manager/test_config.py,sha256=CnoqQ_exrpQdBWVoGxSns3HpccqgmhH9mNJIPTCKKAs,1665
38
+ roboherd/herd/manager/test_config.py,sha256=I2EP7nEUdBVO8Wqot7SRhzp5UZFr5oGIuFS7cNB2iFk,1745
39
39
  roboherd/herd/manager/test_load.py,sha256=zyu5LIChMfTnxu_tYK63-bSOHYn1K1zUlbDY5DkE3GY,514
40
40
  roboherd/herd/manager/test_manager.py,sha256=9pSMaH7zmN-zagYCIBpQcV3Q0sBT7XZSCvsmLVC0rOI,1047
41
- roboherd-0.1.7.dist-info/METADATA,sha256=P95nbFNriZRL6yiODXxuMas9mOQjk8i8PrWYEukG2ug,1007
42
- roboherd-0.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- roboherd-0.1.7.dist-info/entry_points.txt,sha256=WebdVUmh8Ot-FupKJY6Du8LuFbmezt9yoy2UICqV3bE,52
44
- roboherd-0.1.7.dist-info/RECORD,,
41
+ roboherd-0.1.9.dist-info/METADATA,sha256=OPELZHKFq-g1XS1XqUzyvhE6ygMyTutw4YshGFpW3_8,2205
42
+ roboherd-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ roboherd-0.1.9.dist-info/entry_points.txt,sha256=WebdVUmh8Ot-FupKJY6Du8LuFbmezt9yoy2UICqV3bE,52
44
+ roboherd-0.1.9.dist-info/RECORD,,
@@ -1,27 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: roboherd
3
- Version: 0.1.7
4
- Summary: A Fediverse bot framework
5
- Project-URL: Documentation, https://bovine.codeberg.page/roboherd/
6
- Project-URL: Repository, https://codeberg.org/bovine/roboherd
7
- Requires-Python: >=3.11
8
- Requires-Dist: aiohttp>=3.11.12
9
- Requires-Dist: almabtrieb[mqtt]>=0.1.0a1
10
- Requires-Dist: apscheduler>=3.11.0
11
- Requires-Dist: click>=8.1.8
12
- Requires-Dist: cron-descriptor>=1.4.5
13
- Requires-Dist: dynaconf>=3.2.6
14
- Requires-Dist: fast-depends>=2.4.12
15
- Requires-Dist: tomli-w>=1.1.0
16
- Requires-Dist: watchfiles>=1.0.4
17
- Provides-Extra: bovine
18
- Requires-Dist: bovine>=0.5.15; extra == 'bovine'
19
- Requires-Dist: markdown>=3.7; extra == 'bovine'
20
- Description-Content-Type: text/markdown
21
-
22
- # Roboherd
23
-
24
- Roboherd is a framework for building Fediverse bots
25
- using the [Cattle Drive Protocol](https://bovine.codeberg.page/cattle_grid/cattle_drive/).
26
-
27
- For more information, see the [documentation](https://bovine.codeberg.page/roboherd/) or the [repository](https://codeberg.org/bovine/roboherd/).