5
5
import socket
6
6
import subprocess
7
7
import sys
8
- from datetime import datetime
8
+ from datetime import datetime , timezone
9
9
from os import listdir
10
10
from pathlib import Path
11
- from typing import List , Optional
11
+ from typing import Any , Optional
12
12
13
13
import aiohttp
14
+ from aleph_message .models import (
15
+ MessagesResponse ,
16
+ PostMessage ,
17
+ ProgramMessage ,
18
+ StoreMessage ,
19
+ )
20
+ from aleph_message .status import MessageStatus
14
21
from fastapi import FastAPI
15
22
from fastapi .middleware .cors import CORSMiddleware
16
23
from fastapi .responses import PlainTextResponse
17
24
from pip ._internal .operations .freeze import freeze
18
25
from pydantic import BaseModel , HttpUrl
19
26
from starlette .responses import JSONResponse
20
27
28
+ from aleph .sdk .chains .ethereum import get_fallback_account
21
29
from aleph .sdk .chains .remote import RemoteAccount
22
- from aleph .sdk .client import AlephClient , AuthenticatedAlephClient
30
+ from aleph .sdk .client import AlephHttpClient , AuthenticatedAlephHttpClient
31
+ from aleph .sdk .query .filters import MessageFilter
23
32
from aleph .sdk .types import StorageEnum
24
33
from aleph .sdk .vm .app import AlephApp
25
34
from aleph .sdk .vm .cache import VmCache
42
51
43
52
44
53
@app .on_event ("startup" )
45
- async def startup_event ():
54
+ async def startup_event () -> None :
46
55
global startup_lifespan_executed
47
56
startup_lifespan_executed = True
48
57
49
58
50
59
@app .get ("/" )
51
- async def index ():
60
+ async def index () -> dict [ str , Any ] :
52
61
if os .path .exists ("/opt/venv" ):
53
62
opt_venv = list (listdir ("/opt/venv" ))
54
63
else :
55
64
opt_venv = []
56
65
return {
57
66
"Example" : "example_fastapi" ,
58
67
"endpoints" : [
68
+ # Features
69
+ "/lifespan" ,
59
70
"/environ" ,
60
- "/messages" ,
71
+ "/state/increment" ,
72
+ "/wait-for/{delay}" ,
73
+ # Local cache
74
+ "/cache/get/{key}" ,
75
+ "/cache/set/{key}/{value}" ,
76
+ "/cache/remove/{key}" ,
77
+ "/cache/keys" ,
78
+ # Networking
61
79
"/dns" ,
62
- "ip/address" ,
80
+ "/ ip/address" ,
63
81
"/ip/4" ,
64
82
"/ip/6" ,
65
83
"/internet" ,
84
+ # Error handling
85
+ "/raise" ,
86
+ "/crash" ,
87
+ # Aleph.im
88
+ "/messages" ,
89
+ "/get_a_message" ,
66
90
"/post_a_message" ,
67
- "/state/increment" ,
68
- "/wait-for/{delay}" ,
91
+ "/post_a_message_local_account" ,
92
+ "/post_a_file" ,
93
+ "/sign_a_message" ,
94
+ # Platform properties
69
95
"/platform/os" ,
70
96
"/platform/python" ,
71
97
"/platform/pip-freeze" ,
@@ -91,10 +117,11 @@ async def environ() -> dict[str, str]:
91
117
92
118
93
119
@app .get ("/messages" )
94
- async def read_aleph_messages ():
120
+ async def read_aleph_messages () -> dict [ str , MessagesResponse ] :
95
121
"""Read data from Aleph using the Aleph Client library."""
96
- async with AlephClient () as client :
97
- data = await client .get_messages (hashes = ["f246f873c3e0f637a15c566e7a465d2ecbb83eaa024d54ccb8fb566b549a929e" ])
122
+ async with AlephHttpClient () as client :
123
+ message_filter = MessageFilter (hashes = ["f246f873c3e0f637a15c566e7a465d2ecbb83eaa024d54ccb8fb566b549a929e" ])
124
+ data = await client .get_messages (message_filter = message_filter )
98
125
return {"Messages" : data }
99
126
100
127
@@ -163,9 +190,13 @@ async def connect_ipv6():
163
190
if resp .status != 404 :
164
191
resp .raise_for_status ()
165
192
return {"result" : True , "headers" : resp .headers }
166
- except aiohttp .ClientTimeout :
167
- logger .warning (f"Session connection for host { ipv6_host } failed" )
168
- return {"result" : False , "headers" : resp .headers }
193
+ except TimeoutError :
194
+ logger .warning (f"Session connection to host { ipv6_host } timed out" )
195
+ return {"result" : False , "reason" : "Timeout" }
196
+ except aiohttp .ClientConnectionError as error :
197
+ logger .warning (f"Client connection to host { ipv6_host } failed: { error } " )
198
+ # Get a string that describes the error
199
+ return {"result" : False , "reason" : str (error .args [0 ])}
169
200
170
201
171
202
async def check_url (internet_host : HttpUrl , timeout_seconds : int = 5 ):
@@ -184,15 +215,15 @@ async def check_url(internet_host: HttpUrl, timeout_seconds: int = 5):
184
215
@app .get ("/internet" )
185
216
async def read_internet ():
186
217
"""Check Internet connectivity of the system, requiring IP connectivity, domain resolution and HTTPS/TLS."""
187
- internet_hosts : List [HttpUrl ] = [
218
+ internet_hosts : list [HttpUrl ] = [
188
219
HttpUrl (url = "https://aleph.im/" , scheme = "https" ),
189
220
HttpUrl (url = "https://ethereum.org" , scheme = "https" ),
190
221
HttpUrl (url = "https://ipfs.io/" , scheme = "https" ),
191
222
]
192
223
timeout_seconds = 5
193
224
194
225
# Create a list of tasks to check the URLs in parallel
195
- tasks : set [asyncio .Task ] = set ( asyncio .create_task (check_url (host , timeout_seconds )) for host in internet_hosts )
226
+ tasks : set [asyncio .Task ] = { asyncio .create_task (check_url (host , timeout_seconds )) for host in internet_hosts }
196
227
197
228
# While no tasks have completed, keep waiting for the next one to finish
198
229
while tasks :
@@ -211,34 +242,121 @@ async def read_internet():
211
242
return {"result" : False }
212
243
213
244
214
- @app .get ("/post_a_message" )
215
- async def post_a_message ():
216
- """Post a message on the Aleph network"""
245
+ @app .get ("/get_a_message" )
246
+ async def get_a_message ():
247
+ """Get a message from the Aleph.im network"""
248
+ item_hash = "3fc0aa9569da840c43e7bd2033c3c580abb46b007527d6d20f2d4e98e867f7af"
249
+ async with AlephHttpClient () as client :
250
+ message = await client .get_message (
251
+ item_hash = item_hash ,
252
+ message_type = ProgramMessage ,
253
+ )
254
+ return message .dict ()
217
255
218
- account = await RemoteAccount .from_crypto_host (host = "http://localhost" , unix_socket = "/tmp/socat-socket" )
256
+
257
+ @app .post ("/post_a_message" )
258
+ async def post_with_remote_account ():
259
+ """Post a message on the Aleph.im network using the remote account of the host."""
260
+ try :
261
+ account = await RemoteAccount .from_crypto_host (host = "http://localhost" , unix_socket = "/tmp/socat-socket" )
262
+
263
+ content = {
264
+ "date" : datetime .now (tz = timezone .utc ).isoformat (),
265
+ "test" : True ,
266
+ "answer" : 42 ,
267
+ "something" : "interesting" ,
268
+ }
269
+ async with AuthenticatedAlephHttpClient (
270
+ account = account ,
271
+ ) as client :
272
+ message : PostMessage
273
+ status : MessageStatus
274
+ message , status = await client .create_post (
275
+ post_content = content ,
276
+ post_type = "test" ,
277
+ ref = None ,
278
+ channel = "TEST" ,
279
+ inline = True ,
280
+ storage_engine = StorageEnum .storage ,
281
+ sync = True ,
282
+ )
283
+ if status != MessageStatus .PROCESSED :
284
+ return JSONResponse (status_code = 500 , content = {"error" : status })
285
+ return {
286
+ "message" : message ,
287
+ }
288
+ except aiohttp .client_exceptions .UnixClientConnectorError :
289
+ return JSONResponse (status_code = 500 , content = {"error" : "Could not connect to the remote account" })
290
+
291
+
292
+ @app .post ("/post_a_message_local_account" )
293
+ async def post_with_local_account ():
294
+ """Post a message on the Aleph.im network using a local private key."""
295
+
296
+ account = get_fallback_account ()
219
297
220
298
content = {
221
- "date" : datetime .utcnow ( ).isoformat (),
299
+ "date" : datetime .now ( tz = timezone . utc ).isoformat (),
222
300
"test" : True ,
223
301
"answer" : 42 ,
224
302
"something" : "interesting" ,
225
303
}
226
- async with AuthenticatedAlephClient (
304
+ async with AuthenticatedAlephHttpClient (
227
305
account = account ,
306
+ api_server = "https://api2.aleph.im" ,
307
+ allow_unix_sockets = False ,
228
308
) as client :
229
- response = await client .create_post (
309
+ message : PostMessage
310
+ status : MessageStatus
311
+ message , status = await client .create_post (
230
312
post_content = content ,
231
313
post_type = "test" ,
232
314
ref = None ,
233
315
channel = "TEST" ,
234
316
inline = True ,
235
317
storage_engine = StorageEnum .storage ,
318
+ sync = True ,
319
+ )
320
+ if status != MessageStatus .PROCESSED :
321
+ return JSONResponse (status_code = 500 , content = {"error" : status })
322
+ return {
323
+ "message" : message ,
324
+ }
325
+
326
+
327
+ @app .post ("/post_a_file" )
328
+ async def post_a_file ():
329
+ account = get_fallback_account ()
330
+ file_path = Path (__file__ ).absolute ()
331
+ async with AuthenticatedAlephHttpClient (
332
+ account = account ,
333
+ ) as client :
334
+ message : StoreMessage
335
+ status : MessageStatus
336
+ message , status = await client .create_store (
337
+ file_path = file_path ,
338
+ ref = None ,
339
+ channel = "TEST" ,
340
+ storage_engine = StorageEnum .storage ,
341
+ sync = True ,
236
342
)
343
+ if status != MessageStatus .PROCESSED :
344
+ return JSONResponse (status_code = 500 , content = {"error" : status })
237
345
return {
238
- "response " : response ,
346
+ "message " : message ,
239
347
}
240
348
241
349
350
+ @app .get ("/sign_a_message" )
351
+ async def sign_a_message ():
352
+ """Sign a message using a locally managed account within the virtual machine."""
353
+ # FIXME: Broken, fixing this depends on https://github.com/aleph-im/aleph-sdk-python/pull/120
354
+ account = get_fallback_account ()
355
+ message = {"hello" : "world" , "chain" : "ETH" }
356
+ signed_message = await account .sign_message (message )
357
+ return {"message" : signed_message }
358
+
359
+
242
360
@app .get ("/cache/get/{key}" )
243
361
async def get_from_cache (key : str ):
244
362
"""Get data in the VM cache"""
@@ -265,7 +383,7 @@ async def keys_from_cache(pattern: str = "*"):
265
383
266
384
267
385
@app .get ("/state/increment" )
268
- async def increment ():
386
+ async def increment () -> dict [ str , int ] :
269
387
path = "/var/lib/example/storage.json"
270
388
try :
271
389
with open (path ) as fd :
@@ -284,7 +402,7 @@ class Data(BaseModel):
284
402
285
403
286
404
@app .post ("/post" )
287
- async def receive_post (data : Data ):
405
+ async def receive_post (data : Data ) -> str :
288
406
return str (data )
289
407
290
408
@@ -293,13 +411,14 @@ class CustomError(Exception):
293
411
294
412
295
413
@app .get ("/raise" )
296
- def raise_error ():
414
+ def raise_error () -> None :
297
415
"""Raises an error to check that the init handles it properly without crashing"""
298
- raise CustomError ("Whoops" )
416
+ error_message = "Whoops"
417
+ raise CustomError (error_message )
299
418
300
419
301
420
@app .get ("/crash" )
302
- def crash ():
421
+ def crash () -> None :
303
422
"""Crash the entire VM in order to check that the supervisor can handle it"""
304
423
sys .exit (1 )
305
424
@@ -313,22 +432,22 @@ def crash():
313
432
314
433
315
434
@app .get ("/platform/os" )
316
- def platform_os ():
435
+ def platform_os () -> PlainTextResponse :
317
436
return PlainTextResponse (content = Path ("/etc/os-release" ).read_text ())
318
437
319
438
320
439
@app .get ("/platform/python" )
321
- def platform_python ():
440
+ def platform_python () -> PlainTextResponse :
322
441
return PlainTextResponse (content = sys .version )
323
442
324
443
325
444
@app .get ("/platform/pip-freeze" )
326
- def platform_pip_freeze ():
445
+ def platform_pip_freeze () -> list [ str ] :
327
446
return list (freeze ())
328
447
329
448
330
449
@app .event (filters = filters )
331
- async def aleph_event (event ):
450
+ async def aleph_event (event ) -> dict [ str , str ] :
332
451
print ("aleph_event" , event )
333
452
async with aiohttp .ClientSession (connector = aiohttp .TCPConnector ()) as session :
334
453
async with session .get ("https://official.aleph.cloud/api/v0/info/public.json" ) as resp :
0 commit comments