Skip to content

Commit

Permalink
Merge pull request #17 from quadproduction/release/4.0.10
Browse files Browse the repository at this point in the history
release/4.0.10
  • Loading branch information
BenSouchet authored Dec 6, 2024
2 parents 5267378 + 43ecc1b commit ba3c8f1
Show file tree
Hide file tree
Showing 39 changed files with 348 additions and 134 deletions.
29 changes: 29 additions & 0 deletions .run/custom_pre.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="custom_pre" type="PythonConfigurationType" factoryName="Python">
<module name="quadpype" />
<selectedOptions>
<option name="envPaths" visible="false" />
</selectedOptions>
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="_INSIDE_QUADPYPE_TOOL" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/tools/_lib/run/pre_run.py" />
<option name="PARAMETERS" value="-m &quot;mongodb+srv://username:[email protected]/&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="true" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
28 changes: 28 additions & 0 deletions .run/prod.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="prod" type="PythonConfigurationType" factoryName="Python">
<module name="quadpype" />
<selectedOptions>
<option name="environmentVariables" visible="false" />
<option name="envPaths" visible="false" />
</selectedOptions>
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$/src" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$ProjectFileDir$/src/start.py" />
<option name="PARAMETERS" value="tray --debug --additional-env-file $ProjectFileDir$/src/tools/.env" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="true" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2">
<option name="RunConfigurationTask" enabled="true" run_configuration_name="prod_pre" run_configuration_type="PythonConfigurationType" />
</method>
</configuration>
</component>
29 changes: 29 additions & 0 deletions .run/prod_pre.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="prod_pre" type="PythonConfigurationType" factoryName="Python">
<module name="quadpype" />
<selectedOptions>
<option name="envPaths" visible="false" />
</selectedOptions>
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
<env name="_INSIDE_QUADPYPE_TOOL" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/src/tools/_lib/run/pre_run.py" />
<option name="PARAMETERS" value="-p" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="true" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
174 changes: 116 additions & 58 deletions src/quadpype/events/handler.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
"""Module storing the class and logic of the QuadPype Events Handler."""
import os
import time
import json
import functools

from time import sleep
from typing import Optional
from datetime import datetime, timezone, timedelta

import pymongo
import pymongo.errors
import requests
from qtpy import QtCore, QtWidgets

Expand All @@ -21,12 +23,11 @@


_EVENT_HANDLER = None
CHECK_NEW_EVENTS_INTERVAL_SECONDS = 120
MINIMUM_EVENT_EXPIRE_IN_SECONDS = CHECK_NEW_EVENTS_INTERVAL_SECONDS + 10

DEFAULT_RESPONSES_WAITING_TIME_SECS = 3


class EventHandlerWorker(QtCore.QThread):
task_completed = QtCore.Signal(int)

def __init__(self, manager, parent=None):
"""Initialize the worker thread."""
Expand All @@ -39,6 +40,9 @@ def __init__(self, manager, parent=None):

self._webserver_url = self._manager.webserver.url

# Store the amount of time an error occurred
self._error_count = 0

# Get info about the timestamp of the last handled event
self._last_handled_event_timestamp = self._manager.app_registry.get_item(
"last_handled_event_timestamp", fallback=0
Expand All @@ -53,58 +57,106 @@ def __init__(self, manager, parent=None):
self._last_handled_event_timestamp
)

def _respond_to_event(self, event_doc, response):
curr_user_id = self._manager.user_profile["user_id"]
response_data = {
"user_id": curr_user_id,
"content": ""
}

if response.text:
response_content = json.loads(response.text)
if "content" in response_content:
response_data["content"] = response_content["content"]
else:
response_data["content"] = response_content

try:
# Only push the response if the user_id hasn't already sent a response
self._manager.collection.update_one(
{"_id": event_doc["_id"], "responses.user_id": {"$ne": curr_user_id}},
{"$push": {"responses": response_data}}
)
except Exception as e:
print(f"Error updating event: {e}")

def _update_last_handled_event_timestamp(self):
# Update the local app registry
# To keep track and properly retrieve the next events after that
self._manager.app_registry.set_item(
"last_handled_event_timestamp",
get_timestamp_str(self._last_handled_event_timestamp))

def _process_event(self, event_doc):
current_timestamp = datetime.now()

if (event_doc["target_users"] and self._manager.user_profile["user_id"] not in event_doc["target_users"]) or \
(event_doc["target_groups"] and self._manager.user_profile["role"] not in event_doc["target_groups"]) or \
("expire_at" in event_doc and current_timestamp > event_doc["expire_at"]):
# User is not targeted by this event OR
# This event is expired, so we skip it
self._last_handled_event_timestamp = event_doc["timestamp"]
return

route = event_doc["route"]
if not route.startswith("/"):
route = "/" + route

url_with_route = self._webserver_url + route

# Send the event to the webserver API
funct = getattr(requests, event_doc["type"])
if not event_doc["content"]:
response_obj = funct(url_with_route)
else:
response_obj = funct(url_with_route, **event_doc["content"])

if response_obj.status_code == 200 and "responses" in event_doc:
# A response is expected
self._respond_to_event(event_doc, response_obj)

# Update the last handled timestamp
self._last_handled_event_timestamp = event_doc["timestamp"]

def run(self):
start_time = time.time()
# Reset the error count
self._error_count = 0

# Retrieve the new events to process
# Retrieve the new events that were added since the last connection
db_cursor = self._manager.collection.find({
"timestamp": {
"$gt": self._last_handled_event_timestamp
}
}).sort("timestamp", 1)

# Process the events
current_timestamp = datetime.now()
for doc in db_cursor:
# Process these events
for event_doc in db_cursor:
if not self._manager.is_running:
break
if "expire_at" in doc and doc["expire_at"] > current_timestamp:
# This event is expired, skip
self._last_handled_event_timestamp = doc["timestamp"]
continue

route = doc["route"]
if not route.startwith("/"):
route = "/" + route

url_with_route = self._webserver_url + route

# Send the event to the webserver API
funct = getattr(requests, doc["type"])
if not doc["content"]:
response = funct(url_with_route)
else:
response = funct(url_with_route, **doc["content"])

# Update the last handled timestamp
self._last_handled_event_timestamp = doc["timestamp"]
self._update_last_handled_event_timestamp()
return
self._process_event(event_doc)

if db_cursor.retrieved > 0:
# Update the local app registry
# To keep track and properly retrieve the next events after that
self._manager.app_registry.set_item(
"last_handled_event_timestamp",
get_timestamp_str(self._last_handled_event_timestamp))

if not self._manager.is_running:
# Simply exit without emiting the signal
return

elapsed_time = round(time.time() - start_time)
waiting_time = max(0, CHECK_NEW_EVENTS_INTERVAL_SECONDS - elapsed_time)
waiting_time_msec = waiting_time * 1000

self.task_completed.emit(waiting_time_msec)
self._update_last_handled_event_timestamp()

# Watch for new event documents directly on the collection
with self._manager.collection.watch([{"$match": {"operationType": "insert"}}]) as stream:
while self._manager.is_running:
try:
# Non-blocking method to get the next change
change = stream.try_next()
if change:
event_doc = change["fullDocument"]
self._process_event(event_doc)
self._update_last_handled_event_timestamp()
except pymongo.errors.PyMongoError as e:
print(f"Error in change stream: {e}")
self._error_count += 1
if self._error_count > 4:
print("Stopping the Event Handling due to the errors.")
return

sleep(0.2)


class EventsHandlerManager:
Expand Down Expand Up @@ -172,17 +224,13 @@ def _start_worker(self):
self._is_running = True
self._worker_thread.start(QtCore.QThread.HighestPriority)

def _restart_worker(self, waiting_time_msec):
self._timer.start(waiting_time_msec)

def start(self):
if self._worker_thread:
raise RuntimeError("The Event Handler cannot be started multiple times.")
if not self._webserver:
raise RuntimeError("Webserver not set. Cannot start the event handler.")

self._worker_thread = EventHandlerWorker(self)
self._worker_thread.task_completed.connect(self._restart_worker)

if not self._webserver.is_running:
# The webserver is not yet running, wait a bit before starting the loop
Expand Down Expand Up @@ -245,12 +293,14 @@ def _validate_event_values(event_route: str,


@require_events_handler
def register_event(event_route: str,
event_type: str,
content: Optional[dict],
target_users: Optional[list] = None,
target_groups: Optional[list] = None,
expire_in: Optional[int] = None):
def send_event(event_route: str,
event_type: str,
content: Optional[dict],
target_users: Optional[list] = None,
target_groups: Optional[list] = None,
expire_in: Optional[int] = None,
wait_for_responses: bool = False,
waiting_time_secs: int = DEFAULT_RESPONSES_WAITING_TIME_SECS):
if target_users is None:
target_users = []
elif isinstance(target_users, str):
Expand All @@ -268,9 +318,10 @@ def register_event(event_route: str,
if not is_valid:
raise ValueError(invalid_msg)

timestamp = datetime.now()
timestamp = datetime.now(timezone.utc)
event = {
"timestamp": timestamp,
"user_id": _EVENT_HANDLER.user_profile["user_id"],
"route": event_route,
"type": event_type,
"target_users": target_users,
Expand All @@ -279,7 +330,14 @@ def register_event(event_route: str,
}

if expire_in is not None:
expire_in += MINIMUM_EVENT_EXPIRE_IN_SECONDS
event["expire_at"] = timestamp + timedelta(seconds=expire_in)

_EVENT_HANDLER.collection.insert_one(event)
if wait_for_responses:
event["responses"] = []

event_doc_id = _EVENT_HANDLER.collection.insert_one(event).inserted_id

if wait_for_responses:
sleep(waiting_time_secs)
event_doc = _EVENT_HANDLER.collection.find_one({"_id": event_doc_id})
return event_doc["responses"]
1 change: 1 addition & 0 deletions src/quadpype/events/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async def show_tray_message(message: str):
tray_icon_widget.showMessage("Notification", message)
return {"message": "Message displayed"}


@router.post("/popup/", tags=["popup"])
async def show_popup_message(message: str):
tray_icon_widget: Optional[SystemTrayIcon] = get_tray_icon_widget()
Expand Down
2 changes: 1 addition & 1 deletion src/quadpype/hosts/aftereffects/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
defaults = {
"QUADPYPE_LOG_NO_COLORS": "True",
"WEBSOCKET_URL": "ws://localhost:8097/ws/"
"QUADPYPE_WEBSOCKET_URL": "ws://localhost:8017/ws/"
}
for key, value in defaults.items():
if not env.get(key):
Expand Down
2 changes: 1 addition & 1 deletion src/quadpype/hosts/aftereffects/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Expected deployed extension location on default Windows:
For easier debugging of Javascript:
https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1
Add (optional) --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome
then localhost:8092
then localhost:8017

Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01
## Resources
Expand Down
6 changes: 3 additions & 3 deletions src/quadpype/hosts/aftereffects/api/checksums
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
19D4C7C6C4BEA8D552873219030B5CB71D8A8DC2526B7F3909043725E36838EA:.debug
C07F1436D9ED900B3E94605183DB29E82301191D9DC3D55917CF9A68842E6F92:.debug
48F60C67335C984141456712CE8E6F9D82B8C0DF52353571C8D4FCFAB9724A94:index.html
D6CCD9DD044B02E4F2ED1B49E9C083D823D6E3940F9825DD932D07C8B0229C78:css\boilerplate.css
0D183A454C941E5C937DCFDABFFE257F4FBB49BD418DF200AB6C01A39836BA27:css\styles.css
1CE5197F378AD02BCE717B29CB25E7423AFA2E6D22996BABB7297E025D192B4F:css\topcoat-desktop-dark.min.css
4AC922CCE8FC86C9404831E609F3A718A3C0910690E528E413D52FC89810B0A3:CSXS\manifest.xml
F7B0CEE06DDED97BDD89D21B246F80285AC2CFAD205966F39A39F4C644D4D5B6:CSXS\manifest.xml
9F51E857F342D27FF05A97A09E96401EE7EB901C978A31F560F0A49C3A6592FB:icons\ayon_logo.png
A1B1F026B3BB43F4801DB4FAB5CDAE7C1DDCABA064FEDB0A227F633F32EB4F61:icons\iconDarkNormal.png
6C04A66B67CCDA76D4689BF5F80DFB46377E8B3A652CFC5B3D268E9E27DC9853:icons\iconDarkRollover.png
6C04A66B67CCDA76D4689BF5F80DFB46377E8B3A652CFC5B3D268E9E27DC9853:icons\iconDisabled.png
2BEECE898BE17D6843AD22D78139E306DEE28030FC79712B32D99F8487328AB9:icons\iconNormal.png
DD24CF1239E1FFA18B7EE76084E75CE74178CD60A1CF388784934D3DF2F9EED6:icons\iconQuadPype.png
F77A105590E0231818FBAFE766C7A672DC5C6A432E3416594C76D2249D2E978B:icons\iconRollover.png
96F6BD26A8E430D1DE26E2D516DB9DA63328F15936D1F758969524924F9FA5AD:js\main.js
76F230D6FA0F205C83F3FBBC8999D6DBE6A752D5BA7868FE463A1710A20C2C02:js\main.js
423DF7EC0AE7C32886E06C000AE90E3FDAA919DAC563DD9DDEBB787E9E1B6025:js\themeManager.js
5F8F8EBCFFC668009CF48DF1301F8C962A6D54F6EA12F0B0469C42C9DE48F742:js\libs\CSInterface.js
4D9586A075F082A04FD40178499C472012B351DB4C1A4D210907A0891F7D8AD9:js\libs\jquery-2.0.2.min.js
Expand Down
Binary file modified src/quadpype/hosts/aftereffects/api/extension.zxp
Binary file not shown.
Loading

0 comments on commit ba3c8f1

Please sign in to comment.