1
1
import asyncio
2
- import json
3
2
import logging
4
3
import typing
5
4
from datetime import datetime
6
- from functools import partial
7
5
from pathlib import Path
8
6
from typing import (
9
7
Any ,
10
8
AsyncIterable ,
11
9
Coroutine ,
12
10
Dict ,
13
- Generic ,
14
11
Iterable ,
15
12
Iterator ,
16
13
List ,
17
14
Mapping ,
18
15
Optional ,
19
16
Tuple ,
20
17
Type ,
21
- TypeVar ,
22
18
Union ,
23
19
)
24
20
25
- from aleph_message import MessagesResponse , parse_message
26
- from aleph_message .models import (
27
- AlephMessage ,
28
- Chain ,
29
- ItemHash ,
30
- MessageConfirmation ,
31
- MessageType ,
32
- )
21
+ from aleph_message import MessagesResponse
22
+ from aleph_message .models import AlephMessage , Chain , ItemHash , MessageType , PostMessage
33
23
from aleph_message .models .execution .base import Encoding
34
24
from aleph_message .status import MessageStatus
35
- from peewee import (
36
- BooleanField ,
37
- CharField ,
38
- FloatField ,
39
- IntegerField ,
40
- Model ,
41
- SqliteDatabase ,
42
- )
43
- from playhouse .shortcuts import model_to_dict
44
- from playhouse .sqlite_ext import JSONField
45
- from pydantic import BaseModel
46
-
47
- from aleph .sdk import AuthenticatedAlephClient
48
- from aleph .sdk .base import AlephClientBase , AuthenticatedAlephClientBase
49
- from aleph .sdk .conf import settings
50
- from aleph .sdk .exceptions import MessageNotFoundError
51
- from aleph .sdk .models import PostsResponse
52
- from aleph .sdk .types import GenericMessage , StorageEnum
53
-
54
- db = SqliteDatabase (settings .CACHE_DATABASE_PATH )
55
- T = TypeVar ("T" , bound = BaseModel )
56
-
57
-
58
- class JSONDictEncoder (json .JSONEncoder ):
59
- def default (self , obj ):
60
- if isinstance (obj , BaseModel ):
61
- return obj .dict ()
62
- return json .JSONEncoder .default (self , obj )
63
-
64
-
65
- pydantic_json_dumps = partial (json .dumps , cls = JSONDictEncoder )
66
-
67
-
68
- class PydanticField (JSONField , Generic [T ]):
69
- """
70
- A field for storing pydantic model types as JSON in a database. Uses json for serialization.
71
- """
72
-
73
- type : T
74
25
75
- def __init__ (self , * args , ** kwargs ):
76
- self .type = kwargs .pop ("type" )
77
- super ().__init__ (* args , ** kwargs )
26
+ from ..base import BaseAlephClient , BaseAuthenticatedAlephClient
27
+ from ..client import AuthenticatedAlephClient
28
+ from ..conf import settings
29
+ from ..exceptions import MessageNotFoundError
30
+ from ..models import PostsResponse
31
+ from ..types import GenericMessage , StorageEnum
32
+ from .common import db
33
+ from .message import MessageModel , get_message_query , message_to_model , model_to_message
34
+ from .post import PostModel , get_post_query , message_to_post , model_to_post
78
35
79
- def db_value (self , value : Optional [T ]) -> Optional [str ]:
80
- if value is None :
81
- return None
82
- return value .json ()
83
-
84
- def python_value (self , value : Optional [str ]) -> Optional [T ]:
85
- if value is None :
86
- return None
87
- return self .type .parse_raw (value )
88
-
89
-
90
- class MessageModel (Model ):
91
- """
92
- A simple database model for storing AlephMessage objects.
93
- """
94
36
95
- item_hash = CharField (primary_key = True )
96
- chain = CharField (5 )
97
- type = CharField (9 )
98
- sender = CharField ()
99
- channel = CharField (null = True )
100
- confirmations : PydanticField [MessageConfirmation ] = PydanticField (
101
- type = MessageConfirmation , null = True
102
- )
103
- confirmed = BooleanField (null = True )
104
- signature = CharField (null = True )
105
- size = IntegerField (null = True )
106
- time = FloatField ()
107
- item_type = CharField (7 )
108
- item_content = CharField (null = True )
109
- hash_type = CharField (6 , null = True )
110
- content = JSONField (json_dumps = pydantic_json_dumps )
111
- forgotten_by = CharField (null = True )
112
- tags = JSONField (json_dumps = pydantic_json_dumps , null = True )
113
- key = CharField (null = True )
114
- ref = CharField (null = True )
115
- content_type = CharField (null = True )
116
-
117
- class Meta :
118
- database = db
119
-
120
-
121
- def message_to_model (message : AlephMessage ) -> Dict :
122
- return {
123
- "item_hash" : str (message .item_hash ),
124
- "chain" : message .chain ,
125
- "type" : message .type ,
126
- "sender" : message .sender ,
127
- "channel" : message .channel ,
128
- "confirmations" : message .confirmations [0 ] if message .confirmations else None ,
129
- "confirmed" : message .confirmed ,
130
- "signature" : message .signature ,
131
- "size" : message .size ,
132
- "time" : message .time ,
133
- "item_type" : message .item_type ,
134
- "item_content" : message .item_content ,
135
- "hash_type" : message .hash_type ,
136
- "content" : message .content ,
137
- "forgotten_by" : message .forgotten_by [0 ] if message .forgotten_by else None ,
138
- "tags" : message .content .content .get ("tags" , None )
139
- if hasattr (message .content , "content" )
140
- else None ,
141
- "key" : message .content .key if hasattr (message .content , "key" ) else None ,
142
- "ref" : message .content .ref if hasattr (message .content , "ref" ) else None ,
143
- "content_type" : message .content .type
144
- if hasattr (message .content , "type" )
145
- else None ,
146
- }
147
-
148
-
149
- def model_to_message (item : Any ) -> AlephMessage :
150
- item .confirmations = [item .confirmations ] if item .confirmations else []
151
- item .forgotten_by = [item .forgotten_by ] if item .forgotten_by else None
152
-
153
- to_exclude = [
154
- MessageModel .tags ,
155
- MessageModel .ref ,
156
- MessageModel .key ,
157
- MessageModel .content_type ,
158
- ]
159
-
160
- item_dict = model_to_dict (item , exclude = to_exclude )
161
- return parse_message (item_dict )
162
-
163
-
164
- def query_field (field_name , field_values : Iterable [str ]):
165
- field = getattr (MessageModel , field_name )
166
- values = list (field_values )
167
-
168
- if len (values ) == 1 :
169
- return field == values [0 ]
170
- return field .in_ (values )
171
-
172
-
173
- def get_message_query (
174
- message_type : Optional [MessageType ] = None ,
175
- content_keys : Optional [Iterable [str ]] = None ,
176
- content_types : Optional [Iterable [str ]] = None ,
177
- refs : Optional [Iterable [str ]] = None ,
178
- addresses : Optional [Iterable [str ]] = None ,
179
- tags : Optional [Iterable [str ]] = None ,
180
- hashes : Optional [Iterable [str ]] = None ,
181
- channels : Optional [Iterable [str ]] = None ,
182
- chains : Optional [Iterable [str ]] = None ,
183
- start_date : Optional [Union [datetime , float ]] = None ,
184
- end_date : Optional [Union [datetime , float ]] = None ,
185
- ):
186
- query = MessageModel .select ().order_by (MessageModel .time .desc ())
187
- conditions = []
188
- if message_type :
189
- conditions .append (query_field ("type" , [message_type .value ]))
190
- if content_keys :
191
- conditions .append (query_field ("key" , content_keys ))
192
- if content_types :
193
- conditions .append (query_field ("content_type" , content_types ))
194
- if refs :
195
- conditions .append (query_field ("ref" , refs ))
196
- if addresses :
197
- conditions .append (query_field ("sender" , addresses ))
198
- if tags :
199
- for tag in tags :
200
- conditions .append (MessageModel .tags .contains (tag ))
201
- if hashes :
202
- conditions .append (query_field ("item_hash" , hashes ))
203
- if channels :
204
- conditions .append (query_field ("channel" , channels ))
205
- if chains :
206
- conditions .append (query_field ("chain" , chains ))
207
- if start_date :
208
- conditions .append (MessageModel .time >= start_date )
209
- if end_date :
210
- conditions .append (MessageModel .time <= end_date )
211
-
212
- if conditions :
213
- query = query .where (* conditions )
214
- return query
215
-
216
-
217
- class MessageCache (AlephClientBase ):
37
+ class MessageCache (BaseAlephClient ):
218
38
"""
219
39
A wrapper around a sqlite3 database for caching AlephMessage objects.
220
40
221
41
It can be used independently of a DomainNode to implement any kind of caching strategy.
222
42
"""
223
43
224
44
_instance_count = 0 # Class-level counter for active instances
45
+ missing_posts : Dict [ItemHash , PostMessage ] = {}
46
+ """A dict of all posts by item_hash and their amend messages that are missing from the cache."""
225
47
226
48
def __init__ (self ):
227
49
if db .is_closed ():
228
50
db .connect ()
229
51
if not MessageModel .table_exists ():
230
52
db .create_tables ([MessageModel ])
53
+ if not PostModel .table_exists ():
54
+ db .create_tables ([PostModel ])
231
55
232
56
MessageCache ._instance_count += 1
233
57
@@ -270,17 +94,57 @@ def __repr__(self) -> str:
270
94
def __str__ (self ) -> str :
271
95
return repr (self )
272
96
273
- @staticmethod
274
- def add (messages : Union [AlephMessage , Iterable [AlephMessage ]]):
97
+ def add (self , messages : Union [AlephMessage , Iterable [AlephMessage ]]):
275
98
if isinstance (messages , typing .get_args (AlephMessage )):
276
99
messages = [messages ]
277
100
278
- data_source = (message_to_model (message ) for message in messages )
279
- MessageModel .insert_many (data_source ).on_conflict_replace ().execute ()
101
+ message_data = (message_to_model (message ) for message in messages )
102
+ MessageModel .insert_many (message_data ).on_conflict_replace ().execute ()
103
+
104
+ # Add posts and their amends to the PostModel
105
+ post_data = []
106
+ amend_messages = []
107
+ for message in messages :
108
+ if message .item_type != MessageType .post :
109
+ continue
110
+ if message .content .type == "amend" :
111
+ amend_messages .append (message )
112
+ else :
113
+ post = message_to_post (message ).dict ()
114
+ post_data .append (post )
115
+ # Check if we can now add any amend messages that had missing refs
116
+ if message .item_hash in self .missing_posts :
117
+ amend_messages += self .missing_posts .pop (message .item_hash )
118
+
119
+ PostModel .insert_many (post_data ).on_conflict_replace ().execute ()
120
+
121
+ # Handle amends in second step to avoid missing original posts
122
+ post_data = []
123
+ for message in amend_messages :
124
+ # Find the original post and update it
125
+ original_post = MessageModel .get (
126
+ MessageModel .item_hash == message .content .ref
127
+ )
128
+ if not original_post :
129
+ latest_amend = self .missing_posts .get (ItemHash (message .content .ref ))
130
+ if latest_amend and message .time < latest_amend .time :
131
+ self .missing_posts [ItemHash (message .content .ref )] = message
132
+ continue
133
+ if datetime .fromtimestamp (message .time ) < original_post .last_updated :
134
+ continue
135
+ original_post .item_hash = message .item_hash
136
+ original_post .content = message .content .content
137
+ original_post .original_item_hash = message .content .ref
138
+ original_post .original_type = message .content .type
139
+ original_post .address = message .sender
140
+ original_post .channel = message .channel
141
+ original_post .last_updated = datetime .fromtimestamp (message .time )
142
+ post_data .append (original_post )
143
+
144
+ PostModel .insert_many (post_data ).on_conflict_replace ().execute ()
280
145
281
- @staticmethod
282
146
def get (
283
- item_hashes : Union [Union [ItemHash , str ], Iterable [Union [ItemHash , str ]]]
147
+ self , item_hashes : Union [Union [ItemHash , str ], Iterable [Union [ItemHash , str ]]]
284
148
) -> List [AlephMessage ]:
285
149
"""
286
150
Get many messages from the cache by their item hash.
@@ -347,12 +211,11 @@ async def get_posts(
347
211
chains : Optional [Iterable [str ]] = None ,
348
212
start_date : Optional [Union [datetime , float ]] = None ,
349
213
end_date : Optional [Union [datetime , float ]] = None ,
350
- ignore_invalid_messages : bool = True ,
351
- invalid_messages_log_level : int = logging .NOTSET ,
214
+ ignore_invalid_messages : Optional [ bool ] = True ,
215
+ invalid_messages_log_level : Optional [ int ] = logging .NOTSET ,
352
216
) -> PostsResponse :
353
- query = get_message_query (
354
- message_type = MessageType .post ,
355
- content_types = types ,
217
+ query = get_post_query (
218
+ types = types ,
356
219
refs = refs ,
357
220
addresses = addresses ,
358
221
tags = tags ,
@@ -365,7 +228,7 @@ async def get_posts(
365
228
366
229
query = query .paginate (page , pagination )
367
230
368
- posts = [model_to_message (item ) for item in list (query )]
231
+ posts = [model_to_post (item ) for item in list (query )]
369
232
370
233
return PostsResponse (
371
234
posts = posts ,
@@ -383,6 +246,7 @@ async def get_messages(
383
246
pagination : int = 200 ,
384
247
page : int = 1 ,
385
248
message_type : Optional [MessageType ] = None ,
249
+ message_types : Optional [Iterable [MessageType ]] = None ,
386
250
content_types : Optional [Iterable [str ]] = None ,
387
251
content_keys : Optional [Iterable [str ]] = None ,
388
252
refs : Optional [Iterable [str ]] = None ,
@@ -393,14 +257,15 @@ async def get_messages(
393
257
chains : Optional [Iterable [str ]] = None ,
394
258
start_date : Optional [Union [datetime , float ]] = None ,
395
259
end_date : Optional [Union [datetime , float ]] = None ,
396
- ignore_invalid_messages : bool = True ,
397
- invalid_messages_log_level : int = logging .NOTSET ,
260
+ ignore_invalid_messages : Optional [ bool ] = True ,
261
+ invalid_messages_log_level : Optional [ int ] = logging .NOTSET ,
398
262
) -> MessagesResponse :
399
263
"""
400
264
Get many messages from the cache.
401
265
"""
266
+ message_types = message_types or [message_type ] if message_type else None
402
267
query = get_message_query (
403
- message_type = message_type ,
268
+ message_types = message_types ,
404
269
content_keys = content_keys ,
405
270
content_types = content_types ,
406
271
refs = refs ,
@@ -451,6 +316,7 @@ async def get_message(
451
316
async def watch_messages (
452
317
self ,
453
318
message_type : Optional [MessageType ] = None ,
319
+ message_types : Optional [Iterable [MessageType ]] = None ,
454
320
content_types : Optional [Iterable [str ]] = None ,
455
321
content_keys : Optional [Iterable [str ]] = None ,
456
322
refs : Optional [Iterable [str ]] = None ,
@@ -465,8 +331,9 @@ async def watch_messages(
465
331
"""
466
332
Watch messages from the cache.
467
333
"""
334
+ message_types = message_types or [message_type ] if message_type else None
468
335
query = get_message_query (
469
- message_type = message_type ,
336
+ message_types = message_types ,
470
337
content_keys = content_keys ,
471
338
content_types = content_types ,
472
339
refs = refs ,
@@ -483,7 +350,7 @@ async def watch_messages(
483
350
yield model_to_message (item )
484
351
485
352
486
- class DomainNode (MessageCache , AuthenticatedAlephClientBase ):
353
+ class DomainNode (MessageCache , BaseAuthenticatedAlephClient ):
487
354
"""
488
355
A Domain Node is a queryable proxy for Aleph Messages that are stored in a database cache and/or in the Aleph
489
356
network.
0 commit comments