roboherd 0.1.2__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.

@@ -0,0 +1,65 @@
1
+ import pytest
2
+
3
+ from dynaconf import Dynaconf
4
+
5
+ from roboherd.cow import RoboCow
6
+
7
+ from .config import CowConfig, HerdConfig
8
+
9
+
10
+ @pytest.fixture
11
+ def test_config():
12
+ config = Dynaconf()
13
+ config.update(
14
+ {
15
+ "cow": {
16
+ "moocow": {
17
+ "bot": "moocow:bot",
18
+ },
19
+ "rooster": {
20
+ "bot": "rooster:bot",
21
+ },
22
+ },
23
+ }
24
+ )
25
+ return config
26
+
27
+
28
+ def test_from_name_and_dict():
29
+ name = "cow"
30
+ value = {
31
+ "bot": "module:attribute",
32
+ }
33
+
34
+ cow = CowConfig.from_name_and_dict(name, value)
35
+
36
+ assert cow.name == name
37
+ assert cow.module == "module"
38
+ assert cow.attribute == "attribute"
39
+
40
+
41
+ def test_load_config(test_config):
42
+ herd = HerdConfig.from_settings(test_config)
43
+
44
+ assert len(herd.cows) == 2
45
+
46
+ moocow = herd.for_name("moocow")
47
+
48
+ assert moocow.name == "moocow"
49
+
50
+
51
+ def test_load_from_cow_config():
52
+ name = "cow"
53
+ value = {
54
+ "bot": "roboherd.examples.moocow:moocow",
55
+ }
56
+
57
+ cow = CowConfig.from_name_and_dict(name, value)
58
+
59
+ assert isinstance(cow.load(), RoboCow)
60
+
61
+
62
+ def test_names(test_config):
63
+ herd = HerdConfig.from_settings(test_config)
64
+
65
+ assert herd.names == {"moocow", "rooster"}
@@ -0,0 +1,42 @@
1
+ import pytest
2
+ from dynaconf import Dynaconf
3
+
4
+ from almabtrieb.model import ActorInformation
5
+
6
+
7
+ from . import HerdManager
8
+
9
+
10
+ @pytest.fixture
11
+ def test_config():
12
+ config = Dynaconf()
13
+ config.update(
14
+ {
15
+ "cow": {
16
+ "moocow": {
17
+ "bot": "roboherd.examples.moocow:moocow",
18
+ },
19
+ "rooster": {
20
+ "bot": "roboherd.examples.rooster:bot",
21
+ },
22
+ },
23
+ }
24
+ )
25
+ return config
26
+
27
+
28
+ @pytest.mark.parametrize(
29
+ "actor_info,result_lenth",
30
+ [
31
+ ([], 2),
32
+ ([ActorInformation(name="bot:moocow", id="http://host.test/actor")], 1),
33
+ ([ActorInformation(name="moocow", id="http://host.test/actor")], 2),
34
+ ([ActorInformation(name="bot:other", id="http://host.test/actor")], 2),
35
+ ],
36
+ )
37
+ def test_cows_to_create(test_config, actor_info, result_lenth):
38
+ manager = HerdManager.from_settings(test_config)
39
+
40
+ result = manager.cows_to_create(actor_info)
41
+
42
+ assert len(result) == result_lenth
@@ -0,0 +1,39 @@
1
+ import asyncio
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from typing import List
5
+
6
+ from almabtrieb import Almabtrieb
7
+
8
+ from roboherd.cow import RoboCow
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class HerdProcessor:
16
+ connection: Almabtrieb
17
+ incoming_handlers: List[RoboCow]
18
+
19
+ def create_tasks(self, task_group: asyncio.TaskGroup):
20
+ tasks = []
21
+ if len(self.incoming_handlers) > 0:
22
+ tasks.append(task_group.create_task(self.process_incoming(self.connection)))
23
+
24
+ return tasks
25
+
26
+ async def process_incoming(self, connection):
27
+ actor_id_to_cow_map = {}
28
+ for cow in self.incoming_handlers:
29
+ actor_id_to_cow_map[cow.actor_id] = cow
30
+
31
+ async for msg in connection.incoming():
32
+ actor_id = msg["actor"]
33
+
34
+ cow = actor_id_to_cow_map.get(actor_id)
35
+ logger.info(cow)
36
+ if cow:
37
+ await cow.handlers.handle(
38
+ msg, "incoming", connection, actor_id, cow=cow
39
+ )
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from typing import List, Tuple
5
+
6
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
7
+ from apscheduler.triggers.cron import CronTrigger
8
+
9
+ from fast_depends import inject
10
+
11
+ from almabtrieb import Almabtrieb
12
+
13
+ from roboherd.cow import RoboCow, CronEntry
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class HerdScheduler:
20
+ entries: List[Tuple[RoboCow, CronEntry]]
21
+ connection: Almabtrieb
22
+
23
+ def create_task(self, task_group: asyncio.TaskGroup):
24
+ if len(self.entries) == 0:
25
+ logger.info("No tasks to schedule")
26
+ return
27
+ task_group.create_task(self.run())
28
+
29
+ async def run(self):
30
+ if len(self.entries) == 0:
31
+ return
32
+
33
+ scheduler = AsyncIOScheduler()
34
+
35
+ for cow, entry in self.entries:
36
+ trigger = CronTrigger.from_crontab(entry.crontab)
37
+ scheduler.add_job(
38
+ inject(entry.func),
39
+ trigger=trigger,
40
+ kwargs={
41
+ "actor_id": cow.actor_id,
42
+ "connection": self.connection,
43
+ "cow": cow,
44
+ },
45
+ )
46
+
47
+ scheduler.start()
48
+
49
+ while True:
50
+ await asyncio.sleep(60 * 60)
@@ -0,0 +1,37 @@
1
+ import pytest
2
+
3
+ from roboherd.examples.moocow import moocow
4
+ from roboherd.examples.rooster import bot
5
+ from roboherd.examples.dev_null import bot as dev_null
6
+
7
+ from roboherd.cow import CronEntry
8
+
9
+ from . import RoboHerd
10
+
11
+
12
+ @pytest.mark.parametrize("cow, length", [(moocow, 0), (bot, 1)])
13
+ def test_cron_entries(cow, length):
14
+ manager = RoboHerd(cows=[cow])
15
+
16
+ assert len(manager.cron_entries()) == length
17
+
18
+
19
+ def test_cron_entries_result():
20
+ manager = RoboHerd(cows=[bot])
21
+
22
+ result = manager.cron_entries()[0]
23
+
24
+ assert result[0] == bot
25
+ assert isinstance(result[1], CronEntry)
26
+
27
+
28
+ def test_incoming_handlers_no_result():
29
+ manager = RoboHerd(cows=[dev_null])
30
+
31
+ assert len(manager.incoming_handlers()) == 0
32
+
33
+
34
+ def test_incoming_handlers_result():
35
+ manager = RoboHerd(cows=[moocow])
36
+
37
+ assert len(manager.incoming_handlers()) == 1
@@ -0,0 +1,17 @@
1
+ import asyncio
2
+ from unittest.mock import AsyncMock
3
+
4
+ from .scheduler import HerdScheduler
5
+
6
+
7
+ async def test_empty_scheduler_exits():
8
+ scheduler = HerdScheduler(entries=[], connection=AsyncMock())
9
+
10
+ await scheduler.run()
11
+
12
+
13
+ async def test_scheduler_runs_with_task_group_exits():
14
+ scheduler = HerdScheduler(entries=[], connection=AsyncMock())
15
+
16
+ async with asyncio.TaskGroup() as tg:
17
+ scheduler.create_task(tg)
roboherd/herd/types.py ADDED
@@ -0,0 +1,12 @@
1
+ from typing import Callable
2
+ from dataclasses import dataclass
3
+
4
+ from roboherd.cow import RoboCow
5
+
6
+
7
+ @dataclass
8
+ class HandlerInformation:
9
+ action: str
10
+ activity_type: str
11
+ func: Callable
12
+ cow: RoboCow
roboherd/register.py ADDED
@@ -0,0 +1,38 @@
1
+ import aiohttp
2
+ import tomli_w
3
+
4
+
5
+ def create_config(name, password, domain="dev.bovine.social"):
6
+ """
7
+
8
+ ```
9
+ >>> create_config("alice", "pass")
10
+ {'base_url': 'https://dev.bovine.social',
11
+ 'connection_string': 'wss://alice:pass@dev.bovine.social/mooqtt',
12
+ 'echo': False}
13
+
14
+ ```
15
+ """
16
+
17
+ return {
18
+ "base_url": f"https://{domain}",
19
+ "connection_string": f"wss://{name}:{password}@{domain}/mooqtt",
20
+ "echo": False,
21
+ }
22
+
23
+
24
+ async def register(config_file, name, password, fediverse):
25
+ async with aiohttp.ClientSession() as session:
26
+ result = await session.post(
27
+ "https://dev.bovine.social/register",
28
+ json={
29
+ "name": name,
30
+ "password": password,
31
+ "fediverse": fediverse,
32
+ },
33
+ )
34
+
35
+ assert result.status == 201, "Something went wrong"
36
+
37
+ with open(config_file, "wb") as f:
38
+ tomli_w.dump(create_config(name, password), f)
roboherd/test_util.py ADDED
@@ -0,0 +1,24 @@
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"
roboherd/util.py ADDED
@@ -0,0 +1,116 @@
1
+ import click
2
+ import logging
3
+ import copy
4
+ import importlib
5
+ from importlib import import_module
6
+ from urllib.parse import parse_qs, urlparse
7
+
8
+ from almabtrieb import Almabtrieb
9
+
10
+ from roboherd.cow import RoboCow
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def parse_connection_string(connection_string: str) -> dict:
16
+ """
17
+ Parse a connection string into a dictionary of connection parameters.
18
+
19
+ ```pycon
20
+ >>> parse_connection_string("ws://user:pass@host/ws")
21
+ {'host': 'host',
22
+ 'port': 80,
23
+ 'username': 'user',
24
+ 'password': 'pass',
25
+ 'websocket_path': '/ws'}
26
+
27
+ >>> parse_connection_string("wss://user:pass@host/ws")
28
+ {'host': 'host',
29
+ 'port': 443,
30
+ 'username': 'user',
31
+ 'password': 'pass',
32
+ 'websocket_path': '/ws'}
33
+
34
+ ```
35
+ """
36
+
37
+ parsed = urlparse(connection_string)
38
+
39
+ default_port = 80 if parsed.scheme == "ws" else 443
40
+
41
+ return {
42
+ "host": parsed.hostname,
43
+ "port": parsed.port or default_port,
44
+ "username": parsed.username,
45
+ "password": parsed.password,
46
+ "websocket_path": parsed.path,
47
+ }
48
+
49
+
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
+ def create_connection(ctx):
97
+ connection_string = ctx.obj["connection_string"]
98
+ base_url = ctx.obj["base_url"]
99
+
100
+ if not connection_string:
101
+ click.echo("ERROR: No connection string provided")
102
+ click.echo(
103
+ "either provide one through --connection_string or set it in your configuration file"
104
+ )
105
+ exit(1)
106
+
107
+ if not base_url:
108
+ click.echo("ERROR: No base url for cows provided")
109
+ click.echo(
110
+ "either provide one through --base_url or set it in your configuration file"
111
+ )
112
+ exit(1)
113
+
114
+ ctx.obj["connection"] = Almabtrieb.from_connection_string(
115
+ connection_string, echo=ctx.obj["settings"].get("echo", False)
116
+ )
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: roboherd
3
+ Version: 0.1.2
4
+ Summary: A Fediverse bot framework
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: almabtrieb[mqtt]>=0.1.0a1
7
+ Requires-Dist: apscheduler>=3.11.0
8
+ Requires-Dist: click>=8.1.8
9
+ Requires-Dist: cron-descriptor>=1.4.5
10
+ Requires-Dist: dynaconf>=3.2.6
11
+ Requires-Dist: fast-depends>=2.4.12
12
+ Requires-Dist: tomli-w>=1.1.0
13
+ Requires-Dist: watchfiles>=1.0.4
14
+ Provides-Extra: bovine
15
+ Requires-Dist: bovine>=0.5.15; extra == 'bovine'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Roboherd
19
+
20
+ Roboherd is a framework for building Fediverse bots
21
+ using the [Cattle Drive Protocol](https://bovine.codeberg.page/cattle_grid/cattle_drive/).
22
+
23
+ For more information, see the [documentation](https://bovine.codeberg.page/roboherd/) or the [repository](https://codeberg.org/bovine/roboherd/).
@@ -0,0 +1,40 @@
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.2.dist-info/METADATA,sha256=cNehEq5m5RHO7ItfG40HFXYyPCn60hNw3cYJBgToz6E,798
38
+ roboherd-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ roboherd-0.1.2.dist-info/entry_points.txt,sha256=WebdVUmh8Ot-FupKJY6Du8LuFbmezt9yoy2UICqV3bE,52
40
+ roboherd-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ roboherd = roboherd.__main__:main