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.
Files changed (83) hide show
  1. picteus_extension_sdk-0.2.5/PKG-INFO +24 -0
  2. picteus_extension_sdk-0.2.5/README.md +3 -0
  3. picteus_extension_sdk-0.2.5/picteus_extension_sdk/__init__.py +9 -0
  4. picteus_extension_sdk-0.2.5/picteus_extension_sdk/picteus_extension.py +606 -0
  5. picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/PKG-INFO +24 -0
  6. picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/SOURCES.txt +81 -0
  7. picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/dependency_links.txt +1 -0
  8. picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/requires.txt +7 -0
  9. picteus_extension_sdk-0.2.5/picteus_extension_sdk.egg-info/top_level.txt +2 -0
  10. picteus_extension_sdk-0.2.5/picteus_ws_client/__init__.py +172 -0
  11. picteus_extension_sdk-0.2.5/picteus_ws_client/api/__init__.py +12 -0
  12. picteus_extension_sdk-0.2.5/picteus_ws_client/api/administration_api.py +275 -0
  13. picteus_extension_sdk-0.2.5/picteus_ws_client/api/experiment_api.py +283 -0
  14. picteus_extension_sdk-0.2.5/picteus_ws_client/api/extension_api.py +3855 -0
  15. picteus_extension_sdk-0.2.5/picteus_ws_client/api/image_api.py +5435 -0
  16. picteus_extension_sdk-0.2.5/picteus_ws_client/api/image_attachment_api.py +645 -0
  17. picteus_extension_sdk-0.2.5/picteus_ws_client/api/ping_api.py +283 -0
  18. picteus_extension_sdk-0.2.5/picteus_ws_client/api/repository_api.py +3458 -0
  19. picteus_extension_sdk-0.2.5/picteus_ws_client/api/settings_api.py +548 -0
  20. picteus_extension_sdk-0.2.5/picteus_ws_client/api_client.py +802 -0
  21. picteus_extension_sdk-0.2.5/picteus_ws_client/api_response.py +21 -0
  22. picteus_extension_sdk-0.2.5/picteus_ws_client/configuration.py +603 -0
  23. picteus_extension_sdk-0.2.5/picteus_ws_client/exceptions.py +217 -0
  24. picteus_extension_sdk-0.2.5/picteus_ws_client/models/__init__.py +72 -0
  25. picteus_extension_sdk-0.2.5/picteus_ws_client/models/automatic1111_instruction.py +90 -0
  26. picteus_extension_sdk-0.2.5/picteus_ws_client/models/automatic1111_user_comment.py +100 -0
  27. picteus_extension_sdk-0.2.5/picteus_ws_client/models/comfy_ui_prompt_and_workflow.py +90 -0
  28. picteus_extension_sdk-0.2.5/picteus_ws_client/models/command_entity.py +39 -0
  29. picteus_extension_sdk-0.2.5/picteus_ws_client/models/configuration_capability.py +95 -0
  30. picteus_extension_sdk-0.2.5/picteus_ws_client/models/configuration_extension_command.py +102 -0
  31. picteus_extension_sdk-0.2.5/picteus_ws_client/models/dates.py +90 -0
  32. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension.py +95 -0
  33. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_activity.py +91 -0
  34. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_activity_kind.py +39 -0
  35. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_generation_options.py +128 -0
  36. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_image_embeddings.py +98 -0
  37. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_image_feature.py +106 -0
  38. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_image_tag.py +98 -0
  39. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_settings.py +88 -0
  40. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extension_status.py +38 -0
  41. picteus_extension_sdk-0.2.5/picteus_ws_client/models/extensions_configuration.py +106 -0
  42. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image.py +192 -0
  43. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_dimensions.py +91 -0
  44. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_distance.py +95 -0
  45. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_embeddings.py +89 -0
  46. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_feature.py +97 -0
  47. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_feature_format.py +42 -0
  48. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_feature_type.py +40 -0
  49. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_format.py +42 -0
  50. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_generator.py +39 -0
  51. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_metadata.py +100 -0
  52. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_summary.py +166 -0
  53. picteus_extension_sdk-0.2.5/picteus_ws_client/models/image_summary_list.py +99 -0
  54. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest.py +144 -0
  55. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_capability.py +89 -0
  56. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_capability_id.py +40 -0
  57. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_event.py +46 -0
  58. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_execution.py +91 -0
  59. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_extension_command.py +114 -0
  60. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_extension_command_label.py +91 -0
  61. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_extension_command_on.py +91 -0
  62. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_instructions.py +116 -0
  63. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_interface_element.py +99 -0
  64. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_runtime.py +89 -0
  65. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_runtime_environment.py +38 -0
  66. picteus_extension_sdk-0.2.5/picteus_ws_client/models/manifest_user_interface.py +97 -0
  67. picteus_extension_sdk-0.2.5/picteus_ws_client/models/midjourney_instructions.py +102 -0
  68. picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository.py +138 -0
  69. picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_activity.py +99 -0
  70. picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_activity_kind.py +39 -0
  71. picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_location_type.py +37 -0
  72. picteus_extension_sdk-0.2.5/picteus_ws_client/models/repository_status.py +40 -0
  73. picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_criteria.py +98 -0
  74. picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_keyword.py +94 -0
  75. picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_range.py +91 -0
  76. picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_sorting.py +91 -0
  77. picteus_extension_sdk-0.2.5/picteus_ws_client/models/search_sorting_property.py +42 -0
  78. picteus_extension_sdk-0.2.5/picteus_ws_client/models/settings.py +99 -0
  79. picteus_extension_sdk-0.2.5/picteus_ws_client/models/user_interface_anchor.py +39 -0
  80. picteus_extension_sdk-0.2.5/picteus_ws_client/py.typed +0 -0
  81. picteus_extension_sdk-0.2.5/picteus_ws_client/rest.py +259 -0
  82. picteus_extension_sdk-0.2.5/pyproject.toml +35 -0
  83. 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,3 @@
1
+ # Picteus extension Python SDK
2
+
3
+ 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,9 @@
1
+ __version__: str = "0.2.5"
2
+
3
+
4
+ def get_version() -> str:
5
+ """Returns the version of the package."""
6
+ return __version__
7
+
8
+
9
+ from picteus_extension_sdk.picteus_extension import PicteusExtension
@@ -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.