Skip to content

Commit deaa4c3

Browse files
hohodesenfans
authored andcommitted
Feature: Add an API that exposes programs that respond to Aleph messages
Fixes #133
1 parent f8f6c2f commit deaa4c3

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed

src/aleph/utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import asyncio
2+
from copy import copy
23
from hashlib import sha256
3-
from typing import Union
4+
from typing import Union, Dict
45

56
from aleph_message.models import ItemType
67

78
from aleph.exceptions import UnknownHashError
89
from aleph.settings import settings
910

1011

12+
def trim_mongo_id(message: Dict, inplace: bool = True):
13+
"""Remove the MongoDB id of a MongoDB record"""
14+
if '_id' in message:
15+
if inplace is False:
16+
message = copy(message)
17+
message.pop('_id')
18+
return message
19+
20+
1121
async def run_in_executor(executor, func, *args):
1222
if settings.use_executors:
1323
loop = asyncio.get_running_loop()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import json
2+
3+
from aiohttp import web
4+
from aleph_message.models import MessageType
5+
from bson import json_util
6+
from pydantic import BaseModel, ValidationError
7+
8+
from aleph.model.messages import Message
9+
from aleph.utils import trim_mongo_id
10+
11+
12+
class GetProgramQueryFields(BaseModel):
13+
sort_order: int = -1
14+
15+
class Config:
16+
extra = "forbid"
17+
18+
19+
async def get_programs_on_message(request: web.Request) -> web.Response:
20+
try:
21+
query = GetProgramQueryFields(**request.query)
22+
except ValidationError as error:
23+
return web.json_response(
24+
data=error.json(), status=web.HTTPBadRequest.status_code
25+
)
26+
27+
messages = [
28+
trim_mongo_id(msg)
29+
async for msg in Message.collection.find(
30+
filter={
31+
"type": MessageType.program,
32+
"content.on.message": {"$exists": True, "$not": {"$size": 0}},
33+
},
34+
sort=[("time", query.sort_order)],
35+
projection={
36+
"item_hash": 1,
37+
"content.on.message": 1,
38+
},
39+
)
40+
]
41+
42+
response = web.json_response(data=messages)
43+
response.enable_compression()
44+
return response

src/aleph/web/controllers/routes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
storage,
1515
version,
1616
)
17+
from aleph.web.controllers.programs import get_programs_on_message
1718

1819

1920
def register_routes(app: web.Application):
@@ -60,3 +61,5 @@ def register_routes(app: web.Application):
6061

6162
app.router.add_get("/version", version.version)
6263
app.router.add_get("/api/v0/version", version.version)
64+
65+
app.router.add_get("/api/v0/programs/on/message", get_programs_on_message)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
[
2+
{
3+
"chain": "ETH",
4+
"sender": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba",
5+
"type": "PROGRAM",
6+
"channel": "Fun-dApps",
7+
"confirmed": true,
8+
"content": {
9+
"type": "vm-function",
10+
"address": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba",
11+
"allow_amend": false,
12+
"code": {
13+
"encoding": "zip",
14+
"entrypoint": "example_fastapi_2:app",
15+
"ref": "7eb2eca2378ea8855336ed76c8b26219f1cb90234d04441de9cf8cb1c649d003",
16+
"use_latest": false
17+
},
18+
"variables": {
19+
"VM_CUSTOM_VARIABLE": "SOMETHING",
20+
"VM_CUSTOM_VARIABLE_2": "32"
21+
},
22+
"on": {
23+
"http": true,
24+
"message": [
25+
{
26+
"sender": "0xB31B787AdA86c6067701d4C0A250c89C7f1f29A5",
27+
"channel": "TEST"
28+
},
29+
{
30+
"content": {
31+
"ref": "4d4db19afca380fdf06ba7f916153d0f740db9de9eee23ad26ba96a90d8a2920"
32+
}
33+
}
34+
]
35+
},
36+
"environment": {
37+
"reproducible": true,
38+
"internet": false,
39+
"aleph_api": false,
40+
"shared_cache": false
41+
},
42+
"resources": {
43+
"vcpus": 1,
44+
"memory": 128,
45+
"seconds": 30
46+
},
47+
"runtime": {
48+
"ref": "5f31b0706f59404fad3d0bff97ef89ddf24da4761608ea0646329362c662ba51",
49+
"use_latest": false,
50+
"comment": "Aleph Alpine Linux with Python 3.8"
51+
},
52+
"volumes": [
53+
{
54+
"comment": "Python libraries. Read-only since a 'ref' is specified.",
55+
"mount": "/opt/venv",
56+
"ref": "5f31b0706f59404fad3d0bff97ef89ddf24da4761608ea0646329362c662ba51",
57+
"use_latest": false
58+
},
59+
{
60+
"comment": "Ephemeral storage, read-write but will not persist after the VM stops",
61+
"mount": "/var/cache",
62+
"ephemeral": true,
63+
"size_mib": 5
64+
},
65+
{
66+
"comment": "Working data persisted on the VM supervisor, not available on other nodes",
67+
"mount": "/var/lib/sqlite",
68+
"name": "sqlite-data",
69+
"persistence": "host",
70+
"size_mib": 10
71+
},
72+
{
73+
"comment": "Working data persisted on the Aleph network. New VMs will try to use the latest version of this volume, with no guarantee against conflicts",
74+
"mount": "/var/lib/statistics",
75+
"name": "statistics",
76+
"persistence": "store",
77+
"size_mib": 10
78+
},
79+
{
80+
"comment": "Raw drive to use by a process, do not mount it",
81+
"name": "raw-data",
82+
"persistence": "host",
83+
"size_mib": 10
84+
}
85+
],
86+
"data": {
87+
"encoding": "zip",
88+
"mount": "/data",
89+
"ref": "7eb2eca2378ea8855336ed76c8b26219f1cb90234d04441de9cf8cb1c649d003",
90+
"use_latest": false
91+
},
92+
"export": {
93+
"encoding": "zip",
94+
"mount": "/data"
95+
},
96+
"replaces": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba",
97+
"time": 1619017773.8950517
98+
},
99+
"item_type": "inline",
100+
"signature": "0x372da8230552b8c3e65c05b31a0ff3a24666d66c575f8e11019f62579bf48c2b7fe2f0bbe907a2a5bf8050989cdaf8a59ff8a1cbcafcdef0656c54279b4aa0c71b",
101+
"size": 749,
102+
"time": 1619017773.8950577,
103+
"confirmations": [
104+
{
105+
"chain": "ETH",
106+
"height": 12284734,
107+
"hash": "0x67f2f3cde5e94e70615c92629c70d22dc959a118f46e9411b29659c2fce87cdc"
108+
}
109+
]
110+
}
111+
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
from hashlib import sha256
3+
from pathlib import Path
4+
5+
import pytest
6+
import pytest_asyncio
7+
8+
from aleph.model.messages import Message
9+
10+
11+
@pytest_asyncio.fixture
12+
async def fixture_program_message(test_db):
13+
fixtures_file = Path(__file__).parent / "fixtures/messages/program.json"
14+
15+
with fixtures_file.open() as f:
16+
messages = json.load(f)
17+
18+
# Add item_content and item_hash to messages, modify in place:
19+
for message in messages:
20+
if 'item_content' not in message:
21+
message['item_content'] = json.dumps(message['content'])
22+
if 'item_hash' not in message:
23+
message['item_hash'] = sha256(message['item_content'].encode()).hexdigest()
24+
25+
await Message.collection.insert_many(messages)
26+
return messages
27+
28+
29+
@pytest.mark.asyncio
30+
async def test_get_programs_on_message(fixture_program_message, ccn_api_client):
31+
response = await ccn_api_client.get("/api/v0/programs/on/message")
32+
assert response.status == 200, await response.text()
33+
34+
data = await response.json()
35+
expected = {
36+
'item_hash': fixture_program_message[0]['item_hash'],
37+
'content': {'on': {'message': fixture_program_message[0]['content']['on']['message']}},
38+
}
39+
40+
assert data == [expected]

0 commit comments

Comments
 (0)