picteus-extension-sdk 0.2.5__tar.gz
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.
- picteus_extension_sdk-0.2.5/PKG-INFO +24 -0
- picteus_extension_sdk-0.2.5/README.md +3 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk/__init__.py +9 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk/picteus_extension.py +606 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/PKG-INFO +24 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/SOURCES.txt +81 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/dependency_links.txt +1 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/requires.txt +7 -0
- picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/top_level.txt +2 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/__init__.py +172 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/__init__.py +12 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/administration_api.py +275 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/experiment_api.py +283 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/extension_api.py +3855 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/image_api.py +5435 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/image_attachment_api.py +645 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/ping_api.py +283 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/repository_api.py +3458 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api/settings_api.py +548 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api_client.py +802 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/api_response.py +21 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/configuration.py +603 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/exceptions.py +217 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/__init__.py +72 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/automatic1111_instruction.py +90 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/automatic1111_user_comment.py +100 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/comfy_ui_prompt_and_workflow.py +90 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/command_entity.py +39 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/configuration_capability.py +95 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/configuration_extension_command.py +102 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/dates.py +90 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension.py +95 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_activity.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_activity_kind.py +39 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_generation_options.py +128 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_image_embeddings.py +98 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_image_feature.py +106 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_image_tag.py +98 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_settings.py +88 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_status.py +38 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/extensions_configuration.py +106 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image.py +192 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_dimensions.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_distance.py +95 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_embeddings.py +89 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_feature.py +97 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_feature_format.py +42 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_feature_type.py +40 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_format.py +42 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_generator.py +39 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_metadata.py +100 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_summary.py +166 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_summary_list.py +99 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest.py +144 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_capability.py +89 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_capability_id.py +40 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_event.py +46 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_execution.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_extension_command.py +114 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_extension_command_label.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_extension_command_on.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_instructions.py +116 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_interface_element.py +99 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_runtime.py +89 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_runtime_environment.py +38 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_user_interface.py +97 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/midjourney_instructions.py +102 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository.py +138 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_activity.py +99 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_activity_kind.py +39 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_location_type.py +37 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_status.py +40 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_criteria.py +98 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_keyword.py +94 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_range.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_sorting.py +91 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_sorting_property.py +42 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/settings.py +99 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/models/user_interface_anchor.py +39 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/py.typed +0 -0
- picteus_extension_sdk-0.2.5/picteus_ws_client/rest.py +259 -0
- picteus_extension_sdk-0.2.5/pyproject.toml +35 -0
- picteus_extension_sdk-0.2.5/setup.cfg +4 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: picteus-extension-sdk
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: The Picteus extension Python SDK.
|
|
5
|
+
Author-email: Édouard Mercier <edouard@koppasoft.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/picteus/Picteus
|
|
7
|
+
Project-URL: Repository, https://github.com/picteus/Picteus
|
|
8
|
+
Keywords: Picteus,extension
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: python_dateutil>=2.8.2
|
|
15
|
+
Requires-Dist: urllib3<3.0.0,>=2.1.0
|
|
16
|
+
Requires-Dist: pydantic>=2
|
|
17
|
+
Requires-Dist: typing-extensions>=4.7.1
|
|
18
|
+
Requires-Dist: python-socketio[asyncio_client]==5.11.4
|
|
19
|
+
Requires-Dist: aiohttp==3.10.10
|
|
20
|
+
Requires-Dist: requests==2.32.3
|
|
21
|
+
|
|
22
|
+
# Picteus extension Python SDK
|
|
23
|
+
|
|
24
|
+
This package provides the Picteus extension Python SDK, which allows you to interact with the Picteus API and to augment the features of the application by developing an extension.
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from dataclasses import dataclass, asdict
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from logging import getLogger, basicConfig
|
|
12
|
+
from queue import Queue, Empty
|
|
13
|
+
from typing import Dict, Any, Literal, TypeVar, Callable, Optional, Union, List
|
|
14
|
+
|
|
15
|
+
import aiohttp
|
|
16
|
+
import requests
|
|
17
|
+
import socketio
|
|
18
|
+
# noinspection PyPackageRequirements
|
|
19
|
+
import urllib3
|
|
20
|
+
from socketio import SimpleClient
|
|
21
|
+
|
|
22
|
+
import picteus_ws_client
|
|
23
|
+
from picteus_extension_sdk import get_version
|
|
24
|
+
|
|
25
|
+
basicConfig(
|
|
26
|
+
level=logging.DEBUG,
|
|
27
|
+
format="%(asctime)s.%(msecs)03d | %(process)d | %(threadName)s [%(levelname)5s]: %(message)s",
|
|
28
|
+
datefmt="%H:%M:%S")
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
32
|
+
LogLevel = Literal["debug", "info", "warn", "error"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NotificationReturnedErrorCause(StrEnum):
|
|
36
|
+
CANCEL = "cancel",
|
|
37
|
+
ERROR = "error"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NotificationReturnedError(Exception):
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, reason: NotificationReturnedErrorCause) -> None:
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
self.reason: NotificationReturnedErrorCause = reason
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Json = Dict[str, Any]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# In order to benefit from serializable intent structures, taken from https://stackoverflow.com/questions/51286748/make-the-python-json-encoder-support-pythons-new-dataclasses
|
|
51
|
+
@dataclass
|
|
52
|
+
class SuperDataClass:
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def __dict__(self):
|
|
56
|
+
"""
|
|
57
|
+
get a Python dictionary
|
|
58
|
+
"""
|
|
59
|
+
# noinspection PyTypeChecker
|
|
60
|
+
return asdict(self)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def json(self):
|
|
64
|
+
"""
|
|
65
|
+
get the JSON formated string
|
|
66
|
+
"""
|
|
67
|
+
return json.dumps(self.__dict__)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class NotificationsParametersIntent(SuperDataClass):
|
|
72
|
+
parameters: Json
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NotificationsUiAnchor(StrEnum):
|
|
76
|
+
MODAL = "modal",
|
|
77
|
+
SIDEBAR = "sidebar",
|
|
78
|
+
IMAGE_DETAILS = "imageDetail"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class NotificationsUi(SuperDataClass):
|
|
83
|
+
anchor: NotificationsUiAnchor
|
|
84
|
+
url: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class NotificationsUiIntent(SuperDataClass):
|
|
89
|
+
ui: NotificationsUi
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class NotificationsDialogType(StrEnum):
|
|
93
|
+
ERROR = "Error",
|
|
94
|
+
INFO = "Info",
|
|
95
|
+
QUESTION = "Question"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class NotificationsDialogButtons(SuperDataClass):
|
|
100
|
+
yes: str
|
|
101
|
+
no: Optional[str] = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class NotificationsDialog(SuperDataClass):
|
|
106
|
+
type: NotificationsDialogType
|
|
107
|
+
title: str
|
|
108
|
+
description: str
|
|
109
|
+
details: Optional[str]
|
|
110
|
+
buttons: NotificationsDialogButtons
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class NotificationsDialogIntent(SuperDataClass):
|
|
115
|
+
dialog: NotificationsDialog
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class NotificationsImage(SuperDataClass):
|
|
120
|
+
imageId: str
|
|
121
|
+
title: Optional[str] = None
|
|
122
|
+
description: Optional[str] = None
|
|
123
|
+
details: Optional[str] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class NotificationsImages(SuperDataClass):
|
|
128
|
+
images: List[NotificationsImage]
|
|
129
|
+
title: Optional[str] = None
|
|
130
|
+
description: Optional[str] = None
|
|
131
|
+
details: Optional[str] = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class NotificationsImagesIntent(SuperDataClass):
|
|
136
|
+
images: NotificationsImages
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class NotificationsShowType(StrEnum):
|
|
140
|
+
EXTENSION_SETTINGS = "ExtensionSettings"
|
|
141
|
+
IMAGE = "Image"
|
|
142
|
+
REPOSITORY = "Repository"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class NotificationsShow(SuperDataClass):
|
|
147
|
+
type: NotificationsShowType
|
|
148
|
+
id: str
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class NotificationsShowIntent(SuperDataClass):
|
|
153
|
+
show: NotificationsShow
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
NotificationsIntent = Union[
|
|
157
|
+
NotificationsParametersIntent, NotificationsUiIntent, NotificationsDialogIntent, NotificationsImagesIntent, NotificationsShowIntent]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class NotificationEvent(StrEnum):
|
|
161
|
+
PROCESS_RUN_COMMAND = "process.runCommand",
|
|
162
|
+
IMAGE_CREATED = "image.created",
|
|
163
|
+
IMAGE_UPDATED = "image.updated",
|
|
164
|
+
IMAGE_DELETED = "image.deleted",
|
|
165
|
+
IMAGE_COMPUTE_FEATURES = "image.computeFeatures",
|
|
166
|
+
IMAGE_COMPUTE_EMBEDDINGS = "image.computeEmbeddings"
|
|
167
|
+
IMAGE_COMPUTE_TAGS = "image.computeTags",
|
|
168
|
+
IMAGE_RUN_COMMAND = "image.runCommand",
|
|
169
|
+
TEXT_COMPUTE_EMBEDDINGS = "text.computeEmbeddings"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
notificationsChannel = "notifications"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _ExtensionParameters:
|
|
176
|
+
|
|
177
|
+
def __init__(self, parameters: Dict[str, Any]):
|
|
178
|
+
super().__init__()
|
|
179
|
+
self._parameters: Dict[str, Any] = parameters
|
|
180
|
+
self.extension_id: str = parameters.get("extensionId")
|
|
181
|
+
self.web_services_base_url: str = parameters.get("webServicesBaseUrl")
|
|
182
|
+
self.api_key: str = parameters.get("apiKey")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class _MessageSender:
|
|
186
|
+
|
|
187
|
+
def __init__(self, logger: logging.Logger, parameters: _ExtensionParameters,
|
|
188
|
+
sio: Optional[socketio.AsyncClient], socket: Optional[SimpleClient],
|
|
189
|
+
to_string: Callable[[], str], context_id: Optional[str]) -> None:
|
|
190
|
+
super().__init__()
|
|
191
|
+
self.logger: logging.Logger = logger
|
|
192
|
+
self.parameters: _ExtensionParameters = parameters
|
|
193
|
+
self.sio: Optional[socketio.AsyncClient] = sio
|
|
194
|
+
self.socket: Optional[SimpleClient] = socket
|
|
195
|
+
self.to_string: Callable[[], str] = to_string
|
|
196
|
+
self.context_id: Optional[str] = context_id
|
|
197
|
+
|
|
198
|
+
async def send_log(self, message: str, level: LogLevel) -> None:
|
|
199
|
+
match level:
|
|
200
|
+
case "debug":
|
|
201
|
+
log_level = logging.DEBUG
|
|
202
|
+
case "info":
|
|
203
|
+
log_level = logging.INFO
|
|
204
|
+
case "warn":
|
|
205
|
+
log_level = logging.WARN
|
|
206
|
+
case "error":
|
|
207
|
+
log_level = logging.ERROR
|
|
208
|
+
case _:
|
|
209
|
+
log_level = logging.INFO
|
|
210
|
+
self.logger.log(log_level, message)
|
|
211
|
+
await self.send_message(notificationsChannel, {"log": {"message": message, "level": level}})
|
|
212
|
+
|
|
213
|
+
async def send_notification(self, value: Dict[str, Any]) -> None:
|
|
214
|
+
await self.send_message(notificationsChannel, {"notification": value})
|
|
215
|
+
|
|
216
|
+
async def launch_intent(self, intent: NotificationsIntent, future: asyncio.Future) -> None:
|
|
217
|
+
def callback(the_value: [Dict[str, Any]]) -> T:
|
|
218
|
+
if "cancel" in the_value:
|
|
219
|
+
# noinspection PyUnresolvedReferences
|
|
220
|
+
future.set_exception(
|
|
221
|
+
NotificationReturnedError(the_value["cancel"],
|
|
222
|
+
NotificationReturnedErrorCause.CANCEL))
|
|
223
|
+
elif "error" in the_value:
|
|
224
|
+
# noinspection PyUnresolvedReferences
|
|
225
|
+
future.set_exception(
|
|
226
|
+
NotificationReturnedError(the_value["error"],
|
|
227
|
+
NotificationReturnedErrorCause.ERROR))
|
|
228
|
+
else:
|
|
229
|
+
# noinspection PyUnresolvedReferences
|
|
230
|
+
future.set_result(the_value["value"])
|
|
231
|
+
|
|
232
|
+
# Removes recursively the "None" values, faken from https://stackoverflow.com/questions/20558699/python-how-to-recursively-remove-none-values-from-a-nested-data-structure-list
|
|
233
|
+
def remove_none(an_object: T) -> T:
|
|
234
|
+
if isinstance(an_object, (list, tuple, set)):
|
|
235
|
+
return type(an_object)(
|
|
236
|
+
remove_none(object_property) for object_property in an_object if
|
|
237
|
+
object_property is not None)
|
|
238
|
+
elif isinstance(an_object, dict):
|
|
239
|
+
return type(an_object)(
|
|
240
|
+
(remove_none(object_key), remove_none(object_value)) for
|
|
241
|
+
object_key, object_value in
|
|
242
|
+
an_object.items() if
|
|
243
|
+
object_key is not None and object_value is not None)
|
|
244
|
+
else:
|
|
245
|
+
return an_object
|
|
246
|
+
|
|
247
|
+
# We use the "SuperDataClass.__dict__()" method to turn the dataclass instance into a dictionary
|
|
248
|
+
intent_dictionary: Dict[str, Any] = intent.__dict__
|
|
249
|
+
intent_dictionary = remove_none(intent_dictionary)
|
|
250
|
+
body: Dict[str, Any] = {"intent": intent_dictionary}
|
|
251
|
+
await self.send_message(notificationsChannel, body, callback)
|
|
252
|
+
|
|
253
|
+
async def send_acknowledgment(self, success: bool) -> None:
|
|
254
|
+
await self.send_message(notificationsChannel, {"acknowledgment": {"success": success}})
|
|
255
|
+
|
|
256
|
+
async def send_message(self, channel: str, body: Dict[str, Any],
|
|
257
|
+
callback: Callable[[Dict[str, Any]], T] = None) -> None:
|
|
258
|
+
context_id = self.context_id
|
|
259
|
+
self.logger.debug(f"Sending the message {body} on channel '{channel}' for {self.to_string()}" + (
|
|
260
|
+
f" attached to the context with id '{context_id}'" if context_id is not None else "") + (
|
|
261
|
+
" and waiting for a callback" if callback is not None else ""))
|
|
262
|
+
value: Dict[str, Any] = {"apiKey": self.parameters.api_key, "extensionId": self.parameters.extension_id,
|
|
263
|
+
**body}
|
|
264
|
+
if context_id is not None:
|
|
265
|
+
value["contextId"] = context_id
|
|
266
|
+
# noinspection PySimplifyBooleanCheck
|
|
267
|
+
if self.sio is not None:
|
|
268
|
+
await self.sio.emit(event=channel, data=value, namespace=None, callback=callback)
|
|
269
|
+
else:
|
|
270
|
+
await self.socket.emit(channel, value)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Communicator:
|
|
274
|
+
|
|
275
|
+
def __init__(self, logger: logging.Logger, sender: _MessageSender, queue: Queue) -> None:
|
|
276
|
+
super().__init__()
|
|
277
|
+
self.logger: logging.Logger = logger
|
|
278
|
+
self._sender: _MessageSender = sender
|
|
279
|
+
self._queue: Queue = queue
|
|
280
|
+
|
|
281
|
+
def send_log(self, log: str, level: LogLevel) -> None:
|
|
282
|
+
self._queue.put({"sender": self._sender, "type": "log", "log": log, "level": level})
|
|
283
|
+
|
|
284
|
+
def send_notification(self, value: Dict[str, Any]) -> None:
|
|
285
|
+
self._queue.put({"sender": self._sender, "type": "notification", "notification": value})
|
|
286
|
+
|
|
287
|
+
def send_acknowledgment(self, success: bool) -> None:
|
|
288
|
+
self._queue.put({"sender": self._sender, "type": "acknowledgment", "acknowledgment": success})
|
|
289
|
+
|
|
290
|
+
async def launch_intent(self, intent: NotificationsIntent) -> T:
|
|
291
|
+
loop = asyncio.get_event_loop()
|
|
292
|
+
future: asyncio.Future = loop.create_future()
|
|
293
|
+
self._queue.put({"sender": self._sender, "type": "intent", "intent": intent, "future": future})
|
|
294
|
+
# We wait for the future to be set by the callback
|
|
295
|
+
value = await future
|
|
296
|
+
return value
|
|
297
|
+
|
|
298
|
+
async def _send_message(self, channel: str, body: Dict[str, Any],
|
|
299
|
+
callback: Callable[[Dict[str, Any]], T] = None) -> None:
|
|
300
|
+
await self._sender.send_message(channel, body, callback)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class PicteusExtension:
|
|
304
|
+
__notifications = "notifications"
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def get_sdk_version() -> str:
|
|
308
|
+
return get_version()
|
|
309
|
+
|
|
310
|
+
def __init__(self) -> None:
|
|
311
|
+
self.logger: logging.Logger = getLogger(__name__)
|
|
312
|
+
self.logger.info(
|
|
313
|
+
f"Instantiating the {self.to_string()} through the process with id '{os.getpid()}' relying on the SDK version '{PicteusExtension.get_sdk_version()}'")
|
|
314
|
+
self.executor: Optional[ThreadPoolExecutor] = None
|
|
315
|
+
self.queue: Optional[Queue] = None
|
|
316
|
+
self.use_event_driven: bool = True
|
|
317
|
+
# This prevents the warning "InsecureRequestWarning: Unverified HTTPS request is being made.", because we are invoking a local HTTPS endpoint with a self-signed certificate
|
|
318
|
+
urllib3.disable_warnings()
|
|
319
|
+
self.parameters: _ExtensionParameters = _ExtensionParameters(self._get_parameters())
|
|
320
|
+
self.extension_id: str = self.parameters.extension_id
|
|
321
|
+
self.web_services_base_url: str = self.parameters.web_services_base_url
|
|
322
|
+
self.api_key: str = self.parameters.api_key
|
|
323
|
+
self.api_client: picteus_ws_client.ApiClient = self._get_api_web_services_client()
|
|
324
|
+
self.sio: Optional[socketio.AsyncClient] = None
|
|
325
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
326
|
+
self.socket: Optional[SimpleClient] = None
|
|
327
|
+
self.global_communicator: Optional[Communicator] = None
|
|
328
|
+
self.terminating: bool = False
|
|
329
|
+
|
|
330
|
+
async def run(self) -> None:
|
|
331
|
+
self.logger.info(f"Running the {self.to_string()}")
|
|
332
|
+
self.terminating = False
|
|
333
|
+
|
|
334
|
+
self.executor = ThreadPoolExecutor(max_workers=os.cpu_count())
|
|
335
|
+
# We resort to a FIFO queue, so that messages are handled in creation order
|
|
336
|
+
self.queue = Queue()
|
|
337
|
+
|
|
338
|
+
def exception_handler(_loop, context):
|
|
339
|
+
message = context["message"]
|
|
340
|
+
# This is inpired from articles https://superfastpython.com/asyncio-task-exception-was-never-retrieved/ and https://superfastpython.com/asyncio-event-loop-exception-handler
|
|
341
|
+
if message != "Task exception was never retrieved":
|
|
342
|
+
self.logger.error(f"An unexpected exception with message {message} occurred")
|
|
343
|
+
|
|
344
|
+
# We set an exception handler on the running loop
|
|
345
|
+
asyncio.get_running_loop().set_exception_handler(exception_handler)
|
|
346
|
+
|
|
347
|
+
async def on_internal_terminate(signal_number, _stack_frame):
|
|
348
|
+
self.logger.info(f"Received the termination signal '{signal_number}' regarding the {self.to_string()}")
|
|
349
|
+
self.terminating = True
|
|
350
|
+
try:
|
|
351
|
+
await self.on_terminate()
|
|
352
|
+
except Exception as exception:
|
|
353
|
+
self.logger.error(
|
|
354
|
+
f"An error occurred while terminating the {self.to_string()}. Reason: '{str(exception)}'")
|
|
355
|
+
finally:
|
|
356
|
+
try:
|
|
357
|
+
await self._disconnect_socket()
|
|
358
|
+
except Exception as exception:
|
|
359
|
+
self.logger.error(
|
|
360
|
+
f"An error occurred while exiting the {self.to_string()}. Reason: '{str(exception)}'")
|
|
361
|
+
finally:
|
|
362
|
+
self.logger.info(f"Exiting from the {self.to_string()}")
|
|
363
|
+
sys.exit()
|
|
364
|
+
|
|
365
|
+
# We set a "SIGTERM" signal handler
|
|
366
|
+
signal.signal(signal.SIGTERM, lambda signal_number, stack_frame: asyncio.create_task(
|
|
367
|
+
on_internal_terminate(signal_number, stack_frame)))
|
|
368
|
+
|
|
369
|
+
async def pump_log_and_notifications_messages() -> None:
|
|
370
|
+
while True:
|
|
371
|
+
try:
|
|
372
|
+
data = self.queue.get_nowait()
|
|
373
|
+
data_type: str = data["type"]
|
|
374
|
+
try:
|
|
375
|
+
sender: _MessageSender = data["sender"]
|
|
376
|
+
match data_type:
|
|
377
|
+
case "log":
|
|
378
|
+
await sender.send_log(data["log"], data["level"])
|
|
379
|
+
case "notification":
|
|
380
|
+
await sender.send_notification(data["notification"])
|
|
381
|
+
case "intent":
|
|
382
|
+
await sender.launch_intent(data["intent"], data["future"])
|
|
383
|
+
case "acknowledgment":
|
|
384
|
+
await sender.send_acknowledgment(data["acknowledgment"])
|
|
385
|
+
case _:
|
|
386
|
+
self.logger.error(f"Unknown queue message with type '{data_type}'")
|
|
387
|
+
except Exception as exception:
|
|
388
|
+
self.logger.error(
|
|
389
|
+
f"An error occurred while pumping the queue message of type {data_type}. Reason: '{str(exception)}")
|
|
390
|
+
except Empty:
|
|
391
|
+
# This is expected ahd happens when the queue is empty
|
|
392
|
+
pass
|
|
393
|
+
except Exception as exception:
|
|
394
|
+
self.logger.error(f"An error occurred while pumping the queue message. Reason: '{str(exception)}")
|
|
395
|
+
finally:
|
|
396
|
+
await asyncio.sleep(0)
|
|
397
|
+
|
|
398
|
+
asyncio.get_running_loop().create_task(pump_log_and_notifications_messages())
|
|
399
|
+
|
|
400
|
+
async def inner_initialize() -> None:
|
|
401
|
+
# noinspection PySimplifyBooleanCheck
|
|
402
|
+
if (await self.initialize()) == True:
|
|
403
|
+
# noinspection PySimplifyBooleanCheck
|
|
404
|
+
if self.use_event_driven == True:
|
|
405
|
+
await self._connect_socket_event_driven()
|
|
406
|
+
else:
|
|
407
|
+
await self._connect_socket_simple_client()
|
|
408
|
+
else:
|
|
409
|
+
await self.on_ready(None)
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
await inner_initialize()
|
|
413
|
+
finally:
|
|
414
|
+
self.logger.info(f"The {self.to_string()} is now over")
|
|
415
|
+
|
|
416
|
+
def to_string(self) -> str:
|
|
417
|
+
return "extension" + ("" if hasattr(self, 'extension_id') == False else (
|
|
418
|
+
f" with id '{self.extension_id}'")) + f" of class '{self.__class__.__name__}'"
|
|
419
|
+
|
|
420
|
+
# noinspection PyMethodMayBeStatic
|
|
421
|
+
async def initialize(self) -> bool:
|
|
422
|
+
return True
|
|
423
|
+
|
|
424
|
+
async def on_ready(self, communicator: Optional[Communicator]) -> None:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
async def on_terminate(self) -> None:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
431
|
+
async def on_event(self, communicator: Communicator, event: str, value: Dict[str, Any]) -> Any | None:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
async def run_in_executor(self, function: Callable) -> Any | None:
|
|
435
|
+
# noinspection PyTypeChecker
|
|
436
|
+
return await asyncio.get_event_loop().run_in_executor(self.executor, function)
|
|
437
|
+
|
|
438
|
+
def get_repository_api(self) -> picteus_ws_client.RepositoryApi:
|
|
439
|
+
return picteus_ws_client.RepositoryApi(self.api_client)
|
|
440
|
+
|
|
441
|
+
def get_image_api(self) -> picteus_ws_client.ImageApi:
|
|
442
|
+
return picteus_ws_client.ImageApi(self.api_client)
|
|
443
|
+
|
|
444
|
+
def get_image_attachment_api(self) -> picteus_ws_client.ImageAttachmentApi:
|
|
445
|
+
return picteus_ws_client.ImageAttachmentApi(self.api_client)
|
|
446
|
+
|
|
447
|
+
def get_settings(self) -> Dict[str, Any]:
|
|
448
|
+
# noinspection PyUnresolvedReferences
|
|
449
|
+
return picteus_ws_client.ExtensionApi(self.api_client).extension_get_settings(self.extension_id).value
|
|
450
|
+
|
|
451
|
+
# noinspection PyMethodMayBeStatic
|
|
452
|
+
def _get_parameters(self) -> Dict[str, str]:
|
|
453
|
+
with open("parameters.json", "r") as file:
|
|
454
|
+
parameters = json.load(file)
|
|
455
|
+
return parameters
|
|
456
|
+
|
|
457
|
+
async def _connect_socket_event_driven(self) -> None:
|
|
458
|
+
self.logger.info(f"Connecting the {self.to_string()} to the server")
|
|
459
|
+
# The Socket.io Python documentation is available at https://python-socketio.readthedocs.io/en/latest/client.html
|
|
460
|
+
use_ssl: bool = self.web_services_base_url.startswith("https")
|
|
461
|
+
tcp_connector = aiohttp.TCPConnector(ssl=use_ssl, verify_ssl=False if use_ssl else None)
|
|
462
|
+
self.session = aiohttp.ClientSession(connector=tcp_connector)
|
|
463
|
+
self.sio = socketio.AsyncClient(logger=self.logger, http_session=self.session)
|
|
464
|
+
global_sender = _MessageSender(self.logger, self.parameters, self.sio, None, self.to_string, None)
|
|
465
|
+
self.global_communicator = Communicator(self.logger, global_sender, self.queue)
|
|
466
|
+
|
|
467
|
+
@self.sio.event
|
|
468
|
+
async def connect() -> None:
|
|
469
|
+
self.logger.info(f"The {self.to_string()} socket is connected")
|
|
470
|
+
await self.on_ready(self.global_communicator)
|
|
471
|
+
|
|
472
|
+
@self.sio.event
|
|
473
|
+
def connect_error(_data) -> None:
|
|
474
|
+
self.logger.warning(f"The {self.to_string()} socket connection failed")
|
|
475
|
+
|
|
476
|
+
@self.sio.event
|
|
477
|
+
def disconnect() -> None:
|
|
478
|
+
self.logger.info(f"The {self.to_string()} socket is disconnected")
|
|
479
|
+
|
|
480
|
+
@self.sio.on("events")
|
|
481
|
+
async def on_message(event: Dict[str, Any]) -> Any | None:
|
|
482
|
+
command: Dict[str, Any] = event
|
|
483
|
+
channel: str = command["channel"]
|
|
484
|
+
milliseconds: int = command["milliseconds"]
|
|
485
|
+
context_id: str = command["contextId"]
|
|
486
|
+
value: Dict[str, Any] = command["value"]
|
|
487
|
+
timestamp: datetime = datetime.datetime.fromtimestamp(milliseconds / 1000.0, tz=datetime.timezone.utc)
|
|
488
|
+
timestamp_string = timestamp.strftime("%H:%M:%S.%f")[:-3]
|
|
489
|
+
self.logger.info(
|
|
490
|
+
f"The {self.to_string()} received at {timestamp_string} the command {command} on channel '{channel}' attached to the context with id '{context_id}'")
|
|
491
|
+
sender = _MessageSender(self.logger, self.parameters, self.sio, None, self.to_string, context_id)
|
|
492
|
+
communicator = Communicator(self.logger, sender, self.queue)
|
|
493
|
+
|
|
494
|
+
async def handle_event() -> Any | None:
|
|
495
|
+
success: bool = False
|
|
496
|
+
try:
|
|
497
|
+
result: Any | None = await self.on_event(communicator, channel, value)
|
|
498
|
+
success = True
|
|
499
|
+
if result is not None:
|
|
500
|
+
return result
|
|
501
|
+
except Exception as inner_exception:
|
|
502
|
+
# We want the process to continue even if an exception occurs
|
|
503
|
+
self.logger.exception(f"An error occurred during the handling of the event on channel '{channel}'")
|
|
504
|
+
# We use the synchronous variant because we want the events to be handled in creation order
|
|
505
|
+
communicator.send_log(
|
|
506
|
+
f"The handling of the event failed. Reason: '{str(inner_exception)}'",
|
|
507
|
+
"error")
|
|
508
|
+
finally:
|
|
509
|
+
# We use the synchronous variant because we want the events to be handled in creation order
|
|
510
|
+
communicator.send_acknowledgment(success)
|
|
511
|
+
|
|
512
|
+
return await handle_event()
|
|
513
|
+
|
|
514
|
+
await self.sio.connect(self.web_services_base_url, transports=["websocket"])
|
|
515
|
+
await global_sender.send_message("connection",
|
|
516
|
+
{
|
|
517
|
+
"isOpen": True,
|
|
518
|
+
"sdkVersion": PicteusExtension.get_sdk_version(),
|
|
519
|
+
"environment": "python"
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
# noinspection PyBroadException
|
|
523
|
+
try:
|
|
524
|
+
# We wait forever
|
|
525
|
+
await self.sio.wait()
|
|
526
|
+
except Exception as exception:
|
|
527
|
+
# This is expected and this happens when the process is terminated
|
|
528
|
+
# noinspection PySimplifyBooleanCheck
|
|
529
|
+
if self.terminating == True:
|
|
530
|
+
pass
|
|
531
|
+
else:
|
|
532
|
+
self.logger.error(
|
|
533
|
+
f"An error occurred while listening to the server events. Reason: '{str(exception)}'")
|
|
534
|
+
finally:
|
|
535
|
+
# noinspection PySimplifyBooleanCheck
|
|
536
|
+
if self.terminating == False:
|
|
537
|
+
try:
|
|
538
|
+
await self.on_terminate()
|
|
539
|
+
finally:
|
|
540
|
+
await self._disconnect_socket()
|
|
541
|
+
self.logger.debug(f"The {self.to_string()} socket loop is over")
|
|
542
|
+
|
|
543
|
+
async def _connect_socket_simple_client(self) -> None:
|
|
544
|
+
self.logger.info(f"Connecting the {self.to_string()} to the server")
|
|
545
|
+
# The Socket.io Python documentation is available at https://python-socketio.readthedocs.io/en/latest/client.html
|
|
546
|
+
http_session = requests.Session()
|
|
547
|
+
http_session.verify = False
|
|
548
|
+
with SimpleClient(http_session=http_session) as socket:
|
|
549
|
+
socket.connect(self.web_services_base_url, transports=["websocket"])
|
|
550
|
+
self.socket = socket
|
|
551
|
+
global_sender = _MessageSender(self.logger, self.parameters, None, self.socket, self.to_string, None)
|
|
552
|
+
self.global_communicator = Communicator(self.logger, global_sender, self.queue)
|
|
553
|
+
await global_sender.send_message("connection", {"isOpen": True})
|
|
554
|
+
await self.on_ready(self.global_communicator)
|
|
555
|
+
while True:
|
|
556
|
+
try:
|
|
557
|
+
event = socket.receive()
|
|
558
|
+
except Exception as exception:
|
|
559
|
+
# noinspection PySimplifyBooleanCheck
|
|
560
|
+
if self.terminating == True:
|
|
561
|
+
# This is expected
|
|
562
|
+
pass
|
|
563
|
+
else:
|
|
564
|
+
self.logger.error(
|
|
565
|
+
f"An error occurred while listening to the server events. Reason: '{str(exception)}'")
|
|
566
|
+
break
|
|
567
|
+
|
|
568
|
+
command = event[1]
|
|
569
|
+
channel = command["channel"]
|
|
570
|
+
milliseconds: int = command["milliseconds"]
|
|
571
|
+
context_id: str = command["contextId"]
|
|
572
|
+
self.logger.info(
|
|
573
|
+
f"The {self.to_string()} received at {milliseconds} the command {command} on channel '{channel}' attached to the context with id '{context_id}'")
|
|
574
|
+
sender = _MessageSender(self.logger, self.parameters, self.sio, None, self.to_string, context_id)
|
|
575
|
+
communicator = Communicator(self.logger, sender, self.queue)
|
|
576
|
+
success: bool = False
|
|
577
|
+
try:
|
|
578
|
+
await self.on_event(communicator, channel, command["value"])
|
|
579
|
+
success = True
|
|
580
|
+
except Exception as exception:
|
|
581
|
+
# We want the process to continue even if an exception occurs
|
|
582
|
+
self.logger.exception(f"An error occurred during the handling of the event on channel '{channel}'")
|
|
583
|
+
communicator.send_log(f"The handling of the event failed. Reason: '{str(exception)}'",
|
|
584
|
+
"error")
|
|
585
|
+
finally:
|
|
586
|
+
communicator.send_acknowledgment(success)
|
|
587
|
+
|
|
588
|
+
async def _disconnect_socket(self) -> None:
|
|
589
|
+
self.logger.info(f"Disconnecting the {self.to_string()} from the server")
|
|
590
|
+
# noinspection PySimplifyBooleanCheck
|
|
591
|
+
if self.use_event_driven == True:
|
|
592
|
+
if self.sio is not None:
|
|
593
|
+
await self.sio.disconnect()
|
|
594
|
+
self.sio = None
|
|
595
|
+
await self.session.close()
|
|
596
|
+
self.session = None
|
|
597
|
+
else:
|
|
598
|
+
if self.socket is not None:
|
|
599
|
+
self.socket.disconnect()
|
|
600
|
+
self.socket = None
|
|
601
|
+
|
|
602
|
+
def _get_api_web_services_client(self) -> picteus_ws_client.ApiClient:
|
|
603
|
+
configuration = picteus_ws_client.Configuration(host=self.web_services_base_url)
|
|
604
|
+
configuration.verify_ssl = False
|
|
605
|
+
configuration.api_key["api-key"] = self.api_key
|
|
606
|
+
return picteus_ws_client.ApiClient(configuration)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: picteus-extension-sdk
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: The Picteus extension Python SDK.
|
|
5
|
+
Author-email: Édouard Mercier <edouard@koppasoft.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/picteus/Picteus
|
|
7
|
+
Project-URL: Repository, https://github.com/picteus/Picteus
|
|
8
|
+
Keywords: Picteus,extension
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: python_dateutil>=2.8.2
|
|
15
|
+
Requires-Dist: urllib3<3.0.0,>=2.1.0
|
|
16
|
+
Requires-Dist: pydantic>=2
|
|
17
|
+
Requires-Dist: typing-extensions>=4.7.1
|
|
18
|
+
Requires-Dist: python-socketio[asyncio_client]==5.11.4
|
|
19
|
+
Requires-Dist: aiohttp==3.10.10
|
|
20
|
+
Requires-Dist: requests==2.32.3
|
|
21
|
+
|
|
22
|
+
# Picteus extension Python SDK
|
|
23
|
+
|
|
24
|
+
This package provides the Picteus extension Python SDK, which allows you to interact with the Picteus API and to augment the features of the application by developing an extension.
|