para-sdk 0.21.0rc19__cp38-abi3-macosx_11_0_arm64.whl → 0.24.0rc3__cp38-abi3-macosx_11_0_arm64.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 para-sdk might be problematic. Click here for more details.
- para/__init__.py +1 -1
- para/client.py +326 -0
- para/conversation_panel.py +67 -0
- para/messages.py +181 -0
- para/para.abi3.so +0 -0
- para/poller.py +60 -0
- para_sdk-0.24.0rc3.dist-info/METADATA +7 -0
- para_sdk-0.24.0rc3.dist-info/RECORD +10 -0
- {para_sdk-0.21.0rc19.dist-info → para_sdk-0.24.0rc3.dist-info}/WHEEL +1 -1
- para_sdk-0.21.0rc19.dist-info/METADATA +0 -3
- para_sdk-0.21.0rc19.dist-info/RECORD +0 -7
- para_sdk-0.21.0rc19.dist-info/entry_points.txt +0 -2
para/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
from .client import UserClient, new_client, new_connection
|
para/client.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import builtins
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from queue import Empty, Queue
|
|
7
|
+
|
|
8
|
+
from para import para
|
|
9
|
+
|
|
10
|
+
from .poller import Poller
|
|
11
|
+
from .messages import *
|
|
12
|
+
if hasattr(builtins, "__IPYTHON__"):
|
|
13
|
+
from .conversation_panel import ConversationPanel
|
|
14
|
+
|
|
15
|
+
class PncpConversation:
|
|
16
|
+
"""Represents and active or completed conversation.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
"""The unique conversation ID"""
|
|
21
|
+
|
|
22
|
+
messages: list[PncpMessage]
|
|
23
|
+
"""List of messages in the conversation"""
|
|
24
|
+
|
|
25
|
+
_user_client: any
|
|
26
|
+
_complete: bool
|
|
27
|
+
_message_ids: set[str]
|
|
28
|
+
|
|
29
|
+
def __init__(self, id: str, client, actor=None):
|
|
30
|
+
self.id = id
|
|
31
|
+
self._user_client = client
|
|
32
|
+
self._complete = False
|
|
33
|
+
self._message_ids = set()
|
|
34
|
+
self.provider = actor
|
|
35
|
+
self.messages = []
|
|
36
|
+
|
|
37
|
+
def _repr_json_(self):
|
|
38
|
+
meta = {'root': 'conversation', 'expanded': True}
|
|
39
|
+
data = {'id': self.id, 'messages': [m._repr_json_()[0] for m in self.messages]}
|
|
40
|
+
return (data, meta)
|
|
41
|
+
|
|
42
|
+
def _push_pncp(self, msg):
|
|
43
|
+
body = msg['body']
|
|
44
|
+
id = body['messageId']
|
|
45
|
+
if id not in self._message_ids:
|
|
46
|
+
obj = pncp_to_py(msg)
|
|
47
|
+
if obj is not None:
|
|
48
|
+
if isinstance(obj, PncpResponse) or isinstance(obj, PncpError):
|
|
49
|
+
self._complete = True
|
|
50
|
+
self.messages.append(obj)
|
|
51
|
+
self._message_ids.add(obj.id)
|
|
52
|
+
return obj
|
|
53
|
+
|
|
54
|
+
def _push_gql(self, msg):
|
|
55
|
+
id = msg['id']
|
|
56
|
+
if id not in self._message_ids:
|
|
57
|
+
obj = graphql_to_py(msg)
|
|
58
|
+
if obj is not None:
|
|
59
|
+
if isinstance(obj, PncpResponse) or isinstance(obj, PncpError):
|
|
60
|
+
self._complete = True
|
|
61
|
+
self.messages.append(obj)
|
|
62
|
+
self._message_ids.add(obj.id)
|
|
63
|
+
return obj
|
|
64
|
+
|
|
65
|
+
def _sort(self):
|
|
66
|
+
self.messages.sort(key=lambda x: x.created)
|
|
67
|
+
|
|
68
|
+
def dup(self):
|
|
69
|
+
return PncpConversation(self.id, self._user_client, actor=self.provider)
|
|
70
|
+
|
|
71
|
+
def get_last_message(self) -> PncpMessage:
|
|
72
|
+
if len(self.messages) > 0:
|
|
73
|
+
return self.messages[-1]
|
|
74
|
+
|
|
75
|
+
def get_request(self) -> PncpRequest:
|
|
76
|
+
for m in self.messages:
|
|
77
|
+
if isinstance(m, PncpRequest):
|
|
78
|
+
return m
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def get_response(self) -> PncpMessage:
|
|
82
|
+
for m in self.messages:
|
|
83
|
+
if isinstance(m, PncpResponse) or isinstance(m, PncpError):
|
|
84
|
+
return m
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def update(self):
|
|
88
|
+
"""Update `messages` attribute with any new messages.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
if self._complete:
|
|
92
|
+
return
|
|
93
|
+
list_resp = self._user_client._client.list_messages(self.id, 9999)
|
|
94
|
+
for m in list_resp['messages']:
|
|
95
|
+
self._push_gql(m)
|
|
96
|
+
self._sort()
|
|
97
|
+
|
|
98
|
+
def wait_response(self, show_status=False) -> PncpMessage:
|
|
99
|
+
"""Wait for the conversation to complete.
|
|
100
|
+
|
|
101
|
+
Returns the response (`PncpResponse`) and an error (`PncpError`).
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
if not self._complete:
|
|
105
|
+
# start poller before update to ensure no messages are missed
|
|
106
|
+
q = self._user_client._poller.subscribe(id(self), self.id)
|
|
107
|
+
self.update()
|
|
108
|
+
try:
|
|
109
|
+
while not self._complete:
|
|
110
|
+
try:
|
|
111
|
+
item = q.get(timeout=0.5)
|
|
112
|
+
if item['type'] == 'message':
|
|
113
|
+
obj = self._push_pncp(item)
|
|
114
|
+
if show_status and obj is not None:
|
|
115
|
+
if isinstance(obj, PncpStatus):
|
|
116
|
+
print(obj.display_text())
|
|
117
|
+
self._sort()
|
|
118
|
+
except Empty:
|
|
119
|
+
pass
|
|
120
|
+
finally:
|
|
121
|
+
self._user_client._poller.unsubscribe(id(self), self.id)
|
|
122
|
+
return self.get_response()
|
|
123
|
+
|
|
124
|
+
def panel(self, out=None):
|
|
125
|
+
"""Create a live notebook panel that displays conversation status and messages."""
|
|
126
|
+
|
|
127
|
+
if hasattr(builtins, "__IPYTHON__"):
|
|
128
|
+
return make_panel(self, out)
|
|
129
|
+
else:
|
|
130
|
+
raise Exception('Not an ipython environment')
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ConversationStream:
|
|
134
|
+
def __init__(self, conv: PncpConversation):
|
|
135
|
+
self._conversation = conv
|
|
136
|
+
# start poller before update to ensure no messages are missed
|
|
137
|
+
self._queue = conv._user_client._poller.subscribe(id(self), conv.id)
|
|
138
|
+
self._position = 0
|
|
139
|
+
conv.update()
|
|
140
|
+
if conv._complete:
|
|
141
|
+
conv._user_client._poller.unsubscribe(id(self), conv.id)
|
|
142
|
+
|
|
143
|
+
def _poll(self):
|
|
144
|
+
try:
|
|
145
|
+
item = self._queue.get(block=False)
|
|
146
|
+
if item['type'] == 'message':
|
|
147
|
+
self._conversation._push_pncp(item)
|
|
148
|
+
if self._conversation._complete:
|
|
149
|
+
self._conversation._user_client._poller.unsubscribe(id(self), self._conversation.id)
|
|
150
|
+
self._conversation._sort()
|
|
151
|
+
except Empty:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def next(self):
|
|
155
|
+
cur = self._position
|
|
156
|
+
if cur == len(self._conversation.messages):
|
|
157
|
+
if not self._conversation._complete:
|
|
158
|
+
self._poll()
|
|
159
|
+
|
|
160
|
+
if cur < len(self._conversation.messages):
|
|
161
|
+
self._position += 1
|
|
162
|
+
return self._conversation.messages[cur]
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def make_panel(conv: PncpConversation, out) -> ConversationStream:
|
|
166
|
+
# create new instance of PncpConversation panel uses on background thread
|
|
167
|
+
copy = conv.dup()
|
|
168
|
+
stream = ConversationStream(copy)
|
|
169
|
+
p = ConversationPanel(out)
|
|
170
|
+
return p.create(stream)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class ActorList:
|
|
175
|
+
actors: list[dict]
|
|
176
|
+
|
|
177
|
+
def _repr_markdown_(self):
|
|
178
|
+
lines = ['| id | kind |', '| --- | --- |']
|
|
179
|
+
for a in self.actors:
|
|
180
|
+
lines.append(f"| {a['id']} | {a['kind']} |")
|
|
181
|
+
return '\n'.join(lines)
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class SkillList:
|
|
185
|
+
skills: list[dict]
|
|
186
|
+
|
|
187
|
+
def _repr_markdown_(self):
|
|
188
|
+
lines = ['| skillset | subject | action | inputs |', '| --- | --- | --- | --- |']
|
|
189
|
+
for s in self.skills:
|
|
190
|
+
schema = s.get('input_schema')
|
|
191
|
+
inputs = []
|
|
192
|
+
if schema is not None:
|
|
193
|
+
if schema['type'] == 'object':
|
|
194
|
+
for prop in schema['properties']:
|
|
195
|
+
inp = prop
|
|
196
|
+
typ = schema['properties'][prop].get('type')
|
|
197
|
+
if typ is not None:
|
|
198
|
+
inp = inp + ': ' + typ
|
|
199
|
+
inputs.append(inp)
|
|
200
|
+
|
|
201
|
+
lines.append(f"| {s['skillset']} | {s['subject']} | {s['action']} | {', '.join(inputs)} |")
|
|
202
|
+
return '\n'.join(lines)
|
|
203
|
+
|
|
204
|
+
class UserClient:
|
|
205
|
+
"""A build-in instance of this class is created and accessible as `client` within the notebook environment. The client is automatically logged in with your user ID."""
|
|
206
|
+
|
|
207
|
+
_client: any
|
|
208
|
+
_poller: Poller
|
|
209
|
+
|
|
210
|
+
def __init__(self, paranet_client):
|
|
211
|
+
self._client = paranet_client
|
|
212
|
+
self._poller = Poller(paranet_client)
|
|
213
|
+
self._poller.start()
|
|
214
|
+
|
|
215
|
+
def new_request(self, subject: str, action: str, target_actor_id=None, **kwargs) -> PncpConversation:
|
|
216
|
+
"""Send a new skill request.
|
|
217
|
+
|
|
218
|
+
- `subject` The skill request subject.
|
|
219
|
+
- `action` The skill request action.
|
|
220
|
+
- `kwargs` Any number of keyword arguments that are passed as the inputs for the skill request.
|
|
221
|
+
|
|
222
|
+
Returns the active conversation (`PncpConversation`).
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
resp = self._client.skill_request(subject, action, target_actor_id=target_actor_id, **kwargs)
|
|
227
|
+
cid = resp['id'].split('@')[0]
|
|
228
|
+
conv = PncpConversation(cid, self, actor=resp.get('matched_id'))
|
|
229
|
+
conv.update()
|
|
230
|
+
return conv
|
|
231
|
+
|
|
232
|
+
def skill_request(self, subject: str, action: str, target_actor_id=None, **kwargs) -> PncpMessage:
|
|
233
|
+
"""Send a new skill request and wait for the response.
|
|
234
|
+
|
|
235
|
+
- `subject` The skill request subject.
|
|
236
|
+
- `action` The skill request action.
|
|
237
|
+
- `kwargs` Any number of keyword arguments that are passed as the inputs for the skill request.
|
|
238
|
+
|
|
239
|
+
Returns the response (`PncpResponse`) and an error (`PncpError`).
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
resp = self._client.skill_request(subject, action, target_actor_id=target_actor_id, **kwargs)
|
|
244
|
+
cid = resp['id'].split('@')[0]
|
|
245
|
+
conv = PncpConversation(cid, self, actor=resp.get('matched_id'))
|
|
246
|
+
return conv.wait_response(show_status=True)
|
|
247
|
+
|
|
248
|
+
def list_actors(self):
|
|
249
|
+
"""Request a list of registered actors.
|
|
250
|
+
|
|
251
|
+
Returns `ActorList`
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
resp = self.skill_request('paranode', 'list_actors')
|
|
255
|
+
actors = resp.response['actors']
|
|
256
|
+
return ActorList(actors=actors)
|
|
257
|
+
|
|
258
|
+
def list_skills(self):
|
|
259
|
+
"""Request a full list of registered skills.
|
|
260
|
+
|
|
261
|
+
Returns `SkillList`
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
resp = self.skill_request('paranode', 'list_skills')
|
|
265
|
+
skills = resp.response['skills']
|
|
266
|
+
return SkillList(skills=skills)
|
|
267
|
+
|
|
268
|
+
def list_actor_skills(self, actor: str):
|
|
269
|
+
"""Request a list of the given actor's registered skills.
|
|
270
|
+
|
|
271
|
+
`actor` Name of the actor to request skills for.
|
|
272
|
+
|
|
273
|
+
Returns `SkillList`
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
if '@' not in actor:
|
|
277
|
+
actor = actor + '@1.0.0'
|
|
278
|
+
resp = self.skill_request('paranode', 'list_actor_skills', actor=actor)
|
|
279
|
+
skills = resp.response['skills']
|
|
280
|
+
return SkillList(skills=skills)
|
|
281
|
+
|
|
282
|
+
def new_connection(endpoint=None, actor=None, version=None, password=None, jwt=None, token=None, tls_id=None):
|
|
283
|
+
endpoint = endpoint or os.environ['PARANET_ENDPOINT']
|
|
284
|
+
|
|
285
|
+
broker_url = endpoint if endpoint else 'https://paranode:3131'
|
|
286
|
+
service_url = endpoint+'/api/paranet-service' if endpoint else 'https://paranode:3132'
|
|
287
|
+
|
|
288
|
+
actor = actor or os.environ.get('PARANET_ACTOR') or 'root'
|
|
289
|
+
version = version or os.environ.get('PARANET_ACTOR_VERSION') or '1.0.0'
|
|
290
|
+
actor_entity = f'{actor}@{version}'
|
|
291
|
+
password = password or os.environ.get('PARANET_PASSWORD')
|
|
292
|
+
jwt = jwt or os.environ.get('PARANET_JWT')
|
|
293
|
+
token = token or os.environ.get('PARANET_TOKEN')
|
|
294
|
+
|
|
295
|
+
tls_id = tls_id or os.environ.get('PARANET_TLS_ID')
|
|
296
|
+
|
|
297
|
+
connection = para.connect(broker_url, service_url, actor_entity, password, jwt, token, tls_id)
|
|
298
|
+
|
|
299
|
+
print(f'Connected to {broker_url} with actor {actor_entity}')
|
|
300
|
+
|
|
301
|
+
return UserClient(connection)
|
|
302
|
+
|
|
303
|
+
def new_client(proxy=None):
|
|
304
|
+
if 'PARANET_PROXY' in os.environ:
|
|
305
|
+
endpoint = os.environ['PARANET_PROXY']
|
|
306
|
+
broker_url = endpoint
|
|
307
|
+
service_url = endpoint+'/api/paranet-service'
|
|
308
|
+
if endpoint.startswith('http:'):
|
|
309
|
+
client = para.connect(broker_url, service_url, 'root', None)
|
|
310
|
+
return UserClient(client)
|
|
311
|
+
else:
|
|
312
|
+
broker_url = 'https://paranode:3131'
|
|
313
|
+
service_url = 'https://paranode:3132'
|
|
314
|
+
|
|
315
|
+
encoded_token = os.environ.get("PN_PY_USER")
|
|
316
|
+
del os.environ["PN_PY_USER"]
|
|
317
|
+
credentials = json.loads(base64.b64decode(encoded_token))
|
|
318
|
+
actor_id = credentials['id']
|
|
319
|
+
actor = actor_id.split('@')[0]
|
|
320
|
+
access_token = {'access_token': credentials['token'], 'refresh_token': credentials['refresh']}
|
|
321
|
+
client = para.connect(broker_url, service_url, actor, json.dumps(access_token))
|
|
322
|
+
return UserClient(client)
|
|
323
|
+
|
|
324
|
+
if hasattr(builtins, "__IPYTHON__"):
|
|
325
|
+
import panel as pn
|
|
326
|
+
pn.extension()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from typing import cast
|
|
4
|
+
import panel as pn
|
|
5
|
+
|
|
6
|
+
from .messages import *
|
|
7
|
+
|
|
8
|
+
class ConversationPanel:
|
|
9
|
+
def __init__(self, out):
|
|
10
|
+
self._out = out
|
|
11
|
+
|
|
12
|
+
def create(self, stream):
|
|
13
|
+
self._stream = stream
|
|
14
|
+
self._conversation = stream._conversation
|
|
15
|
+
self._recipient = 'actor' if self._conversation.provider is None else self._conversation.provider.split('@')[0]
|
|
16
|
+
self._request = cast(PncpRequest, self._conversation.get_request())
|
|
17
|
+
|
|
18
|
+
self._status = 'Live'
|
|
19
|
+
|
|
20
|
+
# build panel
|
|
21
|
+
self.spinner = pn.indicators.LoadingSpinner(value=True, width=20, height=20, color="primary")
|
|
22
|
+
self.title = pn.pane.Markdown(self._title_display())
|
|
23
|
+
self.status_text = pn.pane.HTML(self._status_display(), width=400)
|
|
24
|
+
self.column = pn.Column(
|
|
25
|
+
self.title,
|
|
26
|
+
self.spinner
|
|
27
|
+
)
|
|
28
|
+
self._insert = 1
|
|
29
|
+
self._poller = pn.state.add_periodic_callback(self._update_status, period=1000)
|
|
30
|
+
return self.column
|
|
31
|
+
|
|
32
|
+
def _title_display(self):
|
|
33
|
+
req = self._request.request
|
|
34
|
+
title = f"### {self._status}: {req['subject']}/{req['action']}"
|
|
35
|
+
if self._conversation.provider is not None:
|
|
36
|
+
title += ' -> ' + self._conversation.provider.split('@')[0]
|
|
37
|
+
return title
|
|
38
|
+
|
|
39
|
+
def _status_display(self):
|
|
40
|
+
return f"<B>{self._status}</B>"
|
|
41
|
+
|
|
42
|
+
def _update_status(self):
|
|
43
|
+
while True:
|
|
44
|
+
obj = self._stream.next()
|
|
45
|
+
if obj is None:
|
|
46
|
+
return
|
|
47
|
+
if self._out is not None:
|
|
48
|
+
self._out.append_stdout(f"got: {obj}\n")
|
|
49
|
+
self._append_message(obj)
|
|
50
|
+
if isinstance(obj, PncpResponse) or isinstance(obj, PncpError):
|
|
51
|
+
self._status = 'Complete' if isinstance(obj, PncpResponse) else 'Failed'
|
|
52
|
+
self.status_text.object = self._status_display()
|
|
53
|
+
self.title.object = self._title_display()
|
|
54
|
+
self.spinner.value = False
|
|
55
|
+
self.column.pop()
|
|
56
|
+
self._poller.stop()
|
|
57
|
+
self._out.append_stdout(f"{self._status}\n")
|
|
58
|
+
|
|
59
|
+
def _append_message(self, msg):
|
|
60
|
+
match msg:
|
|
61
|
+
case PncpRequest():
|
|
62
|
+
party = 'you'
|
|
63
|
+
case _:
|
|
64
|
+
party = self._recipient
|
|
65
|
+
index = self._insert
|
|
66
|
+
self._insert += 1
|
|
67
|
+
self.column.insert(index, pn.pane.HTML(f"({party}) {msg.display_text()}", width=400))
|
para/messages.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import html
|
|
4
|
+
import builtins
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from types import MethodType
|
|
8
|
+
from IPython.core import display_functions
|
|
9
|
+
import mimeparse
|
|
10
|
+
|
|
11
|
+
def value_summary(v):
|
|
12
|
+
if type(v) == list:
|
|
13
|
+
elms = [value_summary(e) for e in v]
|
|
14
|
+
return '[' + ', '.join(elms) + ']'
|
|
15
|
+
elif type(v) == dict:
|
|
16
|
+
elms = []
|
|
17
|
+
for k in v:
|
|
18
|
+
elms.append(k+': ' + value_summary(v[k]))
|
|
19
|
+
return '{' + ', '.join(elms) + '}'
|
|
20
|
+
elif type(v) == str:
|
|
21
|
+
return json.dumps(v)
|
|
22
|
+
return str(v)
|
|
23
|
+
|
|
24
|
+
def data_summary(data):
|
|
25
|
+
if type(data) == dict:
|
|
26
|
+
elms = []
|
|
27
|
+
for k in data:
|
|
28
|
+
elms.append(k+': ' + value_summary(data[k]))
|
|
29
|
+
return ', '.join(elms)
|
|
30
|
+
else:
|
|
31
|
+
return value_summary(data)
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PncpMessage:
|
|
35
|
+
id: str
|
|
36
|
+
created: datetime
|
|
37
|
+
|
|
38
|
+
def display_text(self):
|
|
39
|
+
return f"Unknown message type"
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PncpStatus(PncpMessage):
|
|
43
|
+
status: dict
|
|
44
|
+
|
|
45
|
+
def _repr_json_(self):
|
|
46
|
+
meta = {'root': 'message', 'expanded': True}
|
|
47
|
+
data = {'id': self.id, 'created': self.created, 'status': self.status}
|
|
48
|
+
return (data, meta)
|
|
49
|
+
|
|
50
|
+
def display_text(self):
|
|
51
|
+
keys = list(self.status.keys())
|
|
52
|
+
if len(keys) == 1:
|
|
53
|
+
k = keys[0].lower()
|
|
54
|
+
if k == 'message' or k == 'status':
|
|
55
|
+
return 'Status: ' + data_summary(self.status[keys[0]])
|
|
56
|
+
return 'Status: ' + data_summary(self.status)
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PncpResponse(PncpMessage):
|
|
60
|
+
response: dict
|
|
61
|
+
|
|
62
|
+
def _repr_json_(self):
|
|
63
|
+
meta = {'root': 'message', 'expanded': True}
|
|
64
|
+
data = {'id': self.id, 'created': self.created, 'response': self.response}
|
|
65
|
+
return (data, meta)
|
|
66
|
+
|
|
67
|
+
def display_text(self):
|
|
68
|
+
keys = list(self.response.keys())
|
|
69
|
+
if len(keys) == 1:
|
|
70
|
+
k = keys[0].lower()
|
|
71
|
+
if k == 'message' or k == 'response' or k == 'result':
|
|
72
|
+
return 'Response: ' + data_summary(self.response[keys[0]])
|
|
73
|
+
return 'Response: ' + data_summary(self.response)
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class PncpError(PncpMessage):
|
|
77
|
+
error: dict
|
|
78
|
+
|
|
79
|
+
def _repr_json_(self):
|
|
80
|
+
meta = {'root': 'message', 'expanded': True}
|
|
81
|
+
data = {'id': self.id, 'created': self.created, 'error': self.error}
|
|
82
|
+
return (data, meta)
|
|
83
|
+
|
|
84
|
+
def display_text(self):
|
|
85
|
+
keys = list(self.error.keys())
|
|
86
|
+
if len(keys) == 1:
|
|
87
|
+
k = keys[0].lower()
|
|
88
|
+
if k == 'message' or k == 'error' or k == 'result':
|
|
89
|
+
return 'Error: ' + data_summary(self.error[keys[0]])
|
|
90
|
+
return 'Error: ' + data_summary(self.error)
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class PncpRequest(PncpMessage):
|
|
94
|
+
request: dict
|
|
95
|
+
|
|
96
|
+
def _repr_json_(self):
|
|
97
|
+
meta = {'root': 'message', 'expanded': True}
|
|
98
|
+
request = {'subject': self.request['subject'], 'action': self.request['action'], 'data': self.request['data']}
|
|
99
|
+
data = {'id': self.id, 'created': self.created, 'request': request}
|
|
100
|
+
return (data, meta)
|
|
101
|
+
|
|
102
|
+
def display_text(self):
|
|
103
|
+
args = data_summary(self.request['data'])
|
|
104
|
+
return f"Request: {self.request['subject']}/{self.request['action']}({args})"
|
|
105
|
+
|
|
106
|
+
def get_mime_field(body: dict[str,any]):
|
|
107
|
+
if type(body) == dict:
|
|
108
|
+
for mk in body:
|
|
109
|
+
if mk.startswith('Mime'):
|
|
110
|
+
k = mk[4:]
|
|
111
|
+
k = k[0:1].lower() + k[1:]
|
|
112
|
+
if k in body:
|
|
113
|
+
try:
|
|
114
|
+
mime_type = mimeparse.parse_mime_type(body[mk])
|
|
115
|
+
return (mime_type, body[k])
|
|
116
|
+
except:
|
|
117
|
+
pass
|
|
118
|
+
elif mk.startswith('_mime_'):
|
|
119
|
+
k = mk[6:]
|
|
120
|
+
if k in body:
|
|
121
|
+
try:
|
|
122
|
+
mime_type = mimeparse.parse_mime_type(body[mk])
|
|
123
|
+
return (mime_type, body[k])
|
|
124
|
+
except:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def apply_rendering(msg: PncpMessage, body: dict[str,any]):
|
|
128
|
+
if not hasattr(builtins, "__IPYTHON__"):
|
|
129
|
+
return msg
|
|
130
|
+
|
|
131
|
+
render = get_mime_field(body)
|
|
132
|
+
if render is not None:
|
|
133
|
+
(mime, data) = render
|
|
134
|
+
(base, subtype, params) = mime
|
|
135
|
+
if base == 'application' and subtype == 'vnd.paranet.embed+html':
|
|
136
|
+
width = params.get('width', '100%')
|
|
137
|
+
height = params.get('height', '400')
|
|
138
|
+
iframe = f"<iframe width=\"{width}\" height=\"{height}\" srcdoc=\"{html.escape(data)}\" frameborder=\"0\"></iframe>"
|
|
139
|
+
# Notebook prioritizes json over html, so this method circumvents that by only providing html
|
|
140
|
+
def display(self):
|
|
141
|
+
display_functions.display({'text/html': iframe}, raw=True)
|
|
142
|
+
setattr(msg, '_ipython_display_', display.__get__(msg, msg.__class__))
|
|
143
|
+
|
|
144
|
+
return msg
|
|
145
|
+
|
|
146
|
+
def graphql_to_py(msg):
|
|
147
|
+
id = msg['id']
|
|
148
|
+
created = datetime.fromisoformat(msg['createdAt'].replace("Z", "+00:00"))
|
|
149
|
+
if msg['contents']['packet']['type'] == 'request':
|
|
150
|
+
request = msg['contents']['packet']['body']['body']
|
|
151
|
+
request['data'] = request['body']
|
|
152
|
+
del request['body']
|
|
153
|
+
return PncpRequest(id=id, created=created, request=request)
|
|
154
|
+
elif msg['contents']['packet']['type'] == 'message':
|
|
155
|
+
body = msg['contents']['packet']['body']
|
|
156
|
+
if body['message']['type'] == 'response':
|
|
157
|
+
response = body['message']['body']['data']
|
|
158
|
+
return apply_rendering(PncpResponse(id=id, created=created, response=response), response)
|
|
159
|
+
elif body['message']['type'] == 'status':
|
|
160
|
+
status = body['message']['body']['data']
|
|
161
|
+
return PncpStatus(id=id, created=created, status=status)
|
|
162
|
+
elif body['message']['type'] == 'error':
|
|
163
|
+
error = body['message']['body']['data']
|
|
164
|
+
return PncpError(id=id, created=created, error=error)
|
|
165
|
+
print('Unexpected GQL message', msg, file=sys.stderr)
|
|
166
|
+
|
|
167
|
+
def pncp_to_py(msg):
|
|
168
|
+
if msg['type'] == 'message':
|
|
169
|
+
body = msg['body']
|
|
170
|
+
id = body['messageId']
|
|
171
|
+
created = datetime.fromisoformat(body['timeCreated'].replace("Z", "+00:00"))
|
|
172
|
+
if body['message']['message']['type'] == 'response':
|
|
173
|
+
response = body['message']['message']['body']['data']
|
|
174
|
+
return apply_rendering(PncpResponse(id=id, created=created, response=response), response)
|
|
175
|
+
elif body['message']['message']['type'] == 'status':
|
|
176
|
+
status = body['message']['message']['body']['data']
|
|
177
|
+
return PncpStatus(id=id, created=created, status=status)
|
|
178
|
+
elif body['message']['message']['type'] == 'error':
|
|
179
|
+
error = body['message']['message']['body']['data']
|
|
180
|
+
return PncpError(id=id, created=created, error=error)
|
|
181
|
+
print('Unexpected PNCP message', msg, file=sys.stderr)
|
para/para.abi3.so
CHANGED
|
Binary file
|
para/poller.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from threading import Lock, Thread
|
|
6
|
+
|
|
7
|
+
class Poller:
|
|
8
|
+
_client: any
|
|
9
|
+
_subscribers: dict[str,list[Tuple[int,Queue]]]
|
|
10
|
+
_lock: Lock
|
|
11
|
+
|
|
12
|
+
def __init__(self, client):
|
|
13
|
+
self._client = client
|
|
14
|
+
self._subscribers = {}
|
|
15
|
+
self._lock = Lock()
|
|
16
|
+
|
|
17
|
+
def start(self):
|
|
18
|
+
self._thread = Thread(target=self._mainloop, daemon=True)
|
|
19
|
+
self._thread.start()
|
|
20
|
+
|
|
21
|
+
def _mainloop(self):
|
|
22
|
+
while True:
|
|
23
|
+
resp = self._client.poll_next()
|
|
24
|
+
if resp is not None:
|
|
25
|
+
if resp['type'] == 'message':
|
|
26
|
+
body = resp['body']
|
|
27
|
+
cid = body['id'].split('@')[0]
|
|
28
|
+
if self._lock.acquire():
|
|
29
|
+
try:
|
|
30
|
+
if cid in self._subscribers:
|
|
31
|
+
for (_, q) in self._subscribers[cid]:
|
|
32
|
+
q.put(resp)
|
|
33
|
+
except Exception as ex:
|
|
34
|
+
print(ex)
|
|
35
|
+
self._lock.release()
|
|
36
|
+
else:
|
|
37
|
+
time.sleep(0.1)
|
|
38
|
+
|
|
39
|
+
def subscribe(self, uid: int, cid: str) -> Queue:
|
|
40
|
+
if self._lock.acquire():
|
|
41
|
+
try:
|
|
42
|
+
if cid not in self._subscribers:
|
|
43
|
+
self._subscribers[cid] = []
|
|
44
|
+
q = Queue()
|
|
45
|
+
self._subscribers[cid].append((uid, q))
|
|
46
|
+
except Exception as ex:
|
|
47
|
+
print(ex)
|
|
48
|
+
self._lock.release()
|
|
49
|
+
return q
|
|
50
|
+
|
|
51
|
+
def unsubscribe(self, uid: int, cid: str):
|
|
52
|
+
if self._lock.acquire():
|
|
53
|
+
try:
|
|
54
|
+
if cid in self._subscribers:
|
|
55
|
+
self._subscribers[cid] = [sub for sub in self._subscribers[cid] if sub[0] != uid]
|
|
56
|
+
if len(self._subscribers[cid]) == 0:
|
|
57
|
+
del self._subscribers[cid]
|
|
58
|
+
except Exception as ex:
|
|
59
|
+
print(ex)
|
|
60
|
+
self._lock.release()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
para/__init__.py,sha256=j91SUu6B2VlgXvHvNkH5NK3WDkBdrNBytoPhO5E_LWU,58
|
|
2
|
+
para/__main__.py,sha256=GUDEQZo7PdWXRCYbNZm65kcl0lENtu3AikKs0bflDrM,64
|
|
3
|
+
para/client.py,sha256=-L3BQ54fumiD06vW97etNcPbFgUA6qIjJUu0YF6Ng8A,10141
|
|
4
|
+
para/conversation_panel.py,sha256=cGRdl2ay1BJJ1yOsJTunDgJAY7CD4BczTiWE0VzN3xc,2168
|
|
5
|
+
para/messages.py,sha256=U41mSpPXp9ub5JpUU53hn-yha9dCA9qOxrj9vsGQUHY,6102
|
|
6
|
+
para/para.abi3.so,sha256=AT7EOMd9lgCYdXrIcY7OqTj_ZEZJMYRgNv4mV04jvMo,55710368
|
|
7
|
+
para/poller.py,sha256=vmUeA_Gj9xD7O15n9tY8w37QMMHZwfJpbShB9lNHoJ4,1666
|
|
8
|
+
para_sdk-0.24.0rc3.dist-info/METADATA,sha256=CH_xRmE5s_ZasnPeqRge2C3fQZijmZTxZ-RVV-teoik,154
|
|
9
|
+
para_sdk-0.24.0rc3.dist-info/WHEEL,sha256=gnJiauUA-C9eduukCSYPRAnEnckuUqP6yenZ8c1xV7c,102
|
|
10
|
+
para_sdk-0.24.0rc3.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
para/__init__.py,sha256=8nY2C-MRakbnN1mtZlVfrTj96QQAJ0FqPtuPL6pGF_U,23
|
|
2
|
-
para/__main__.py,sha256=GUDEQZo7PdWXRCYbNZm65kcl0lENtu3AikKs0bflDrM,64
|
|
3
|
-
para/para.abi3.so,sha256=S60ghahMIhl0Fj_52aDlRtQayeknOy-ocjuP3znVibw,48266528
|
|
4
|
-
para_sdk-0.21.0rc19.dist-info/METADATA,sha256=CGdiF01vZRX_JnAyioGIxb1bNPXOjbaSgV_3-6FVsB0,57
|
|
5
|
-
para_sdk-0.21.0rc19.dist-info/WHEEL,sha256=tDwpVQdIftVdUCGzi9EZxv0g6xpy3EOhhp1xDnIF2OU,102
|
|
6
|
-
para_sdk-0.21.0rc19.dist-info/entry_points.txt,sha256=px5FZ1yAAKZ9kGJTb7564AHax4LzD90r73E5_shEwkU,33
|
|
7
|
-
para_sdk-0.21.0rc19.dist-info/RECORD,,
|