zou 0.20.39__py3-none-any.whl → 0.20.41__py3-none-any.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.
zou/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.20.39"
1
+ __version__ = "0.20.41"
zou/app/api.py CHANGED
@@ -1,13 +1,10 @@
1
1
  import os
2
2
  import sys
3
- import tomlkit
4
3
  import importlib
5
4
  import traceback
6
5
 
7
- from zou.app.utils import events
8
6
  from pathlib import Path
9
7
 
10
-
11
8
  from zou.app.blueprints.assets import blueprint as assets_blueprint
12
9
  from zou.app.blueprints.auth import blueprint as auth_blueprint
13
10
  from zou.app.blueprints.breakdown import blueprint as breakdown_blueprint
@@ -32,6 +29,9 @@ from zou.app.blueprints.user import blueprint as user_blueprint
32
29
  from zou.app.blueprints.edits import blueprint as edits_blueprint
33
30
  from zou.app.blueprints.concepts import blueprint as concepts_blueprint
34
31
 
32
+ from zou.app.utils.plugins import PluginManifest
33
+ from zou.app.utils import events
34
+
35
35
 
36
36
  def configure(app):
37
37
  """
@@ -98,30 +98,34 @@ def load_plugins(app):
98
98
  """
99
99
  Load plugins from the plugin folder.
100
100
  """
101
- abs_plugin_path = os.path.abspath(app.config["PLUGIN_FOLDER"])
101
+ plugin_folder = app.config["PLUGIN_FOLDER"]
102
+ abs_plugin_path = os.path.abspath(plugin_folder)
102
103
  if abs_plugin_path not in sys.path:
103
104
  sys.path.insert(0, abs_plugin_path)
104
105
 
105
- if os.path.exists(app.config["PLUGIN_FOLDER"]):
106
- for plugin_id in os.listdir(app.config["PLUGIN_FOLDER"]):
106
+ if os.path.exists(plugin_folder):
107
+ for plugin_id in os.listdir(plugin_folder):
107
108
  try:
108
109
  load_plugin(app, plugin_id)
109
110
  app.logger.info(f"Plugin {plugin_id} loaded.")
110
- except:
111
- app.logger.error(f"Plugin {plugin_id} fails to load:")
112
- app.logger.error(traceback.format_exc())
111
+ except ImportError as e:
112
+ app.logger.error(f"Plugin {plugin_id} failed to import: {e}")
113
+ except Exception as e:
114
+ app.logger.error(
115
+ f"Plugin {plugin_id} failed to initialize: {e}"
116
+ )
117
+ app.logger.debug(traceback.format_exc())
113
118
 
114
119
  if abs_plugin_path in sys.path:
115
120
  sys.path.remove(abs_plugin_path)
116
121
 
117
122
 
118
123
  def load_plugin(app, plugin_id):
119
- with open(
120
- Path(app.config["PLUGIN_FOLDER"]).joinpath(plugin_id, "manifest.toml"),
121
- "rb",
122
- ) as manifest_file:
123
- manifest = tomlkit.load(manifest_file)
124
+ plugin_path = Path(app.config["PLUGIN_FOLDER"]) / plugin_id
125
+ manifest = PluginManifest.from_file(plugin_path / "manifest.toml")
126
+
124
127
  plugin_module = importlib.import_module(plugin_id)
125
128
  if hasattr(plugin_module, "init_plugin"):
126
129
  plugin_module.init_plugin(app, manifest)
130
+
127
131
  return plugin_module
@@ -134,6 +134,8 @@ from zou.app.blueprints.crud.salary_scale import (
134
134
  SalaryScaleResource,
135
135
  )
136
136
 
137
+ from zou.app.blueprints.crud.plugin import PluginResource, PluginsResource
138
+
137
139
  routes = [
138
140
  ("/data/persons", PersonsResource),
139
141
  ("/data/persons/<instance_id>", PersonResource),
@@ -218,6 +220,8 @@ routes = [
218
220
  ("/data/studios/<instance_id>", StudioResource),
219
221
  ("/data/salary-scales", SalaryScalesResource),
220
222
  ("/data/salary-scales/<instance_id>", SalaryScaleResource),
223
+ ("/data/plugins/<instance_id>", PluginResource),
224
+ ("/data/plugins", PluginsResource),
221
225
  ]
222
226
 
223
227
  blueprint = Blueprint("/data", "data")
@@ -0,0 +1,169 @@
1
+ import zipfile
2
+ import semver
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from zou.app import config, db
7
+ from zou.app.models.plugin import Plugin
8
+ from zou.app.utils.plugins import PluginManifest
9
+
10
+
11
+ def install_plugin(path, force=False):
12
+ """
13
+ Install a plugin.
14
+ """
15
+ path = Path(path)
16
+ if not path.exists():
17
+ raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
18
+
19
+ manifest = PluginManifest.from_plugin_path(path)
20
+ plugin = Plugin.query.filter_by(plugin_id=manifest.id).one_or_none()
21
+
22
+ try:
23
+ already_installed = False
24
+ if plugin:
25
+ current = semver.Version.parse(plugin.version)
26
+ new = semver.Version.parse(str(manifest.version))
27
+ if not force and new <= current:
28
+ raise ValueError(
29
+ f"Plugin version {new} is not newer than {current}."
30
+ )
31
+ plugin.update_no_commit(manifest.to_model_dict())
32
+ already_installed = True
33
+ else:
34
+ plugin = Plugin.create_no_commit(**manifest.to_model_dict())
35
+
36
+ install_plugin_files(manifest.id, path, already_installed)
37
+ except Exception:
38
+ uninstall_plugin_files(manifest.id)
39
+ db.session.rollback()
40
+ db.session.remove()
41
+ raise
42
+
43
+ Plugin.commit()
44
+ return plugin.serialize()
45
+
46
+
47
+ def install_plugin_files(plugin_id, path, already_installed=False):
48
+ """
49
+ Install plugin files.
50
+ """
51
+ path = Path(path)
52
+ plugin_path = Path(config.PLUGIN_FOLDER) / plugin_id
53
+ if already_installed and plugin_path.exists():
54
+ shutil.rmtree(plugin_path)
55
+
56
+ plugin_path.mkdir(parents=True, exist_ok=True)
57
+
58
+ if path.is_dir():
59
+ shutil.copytree(path, plugin_path, dirs_exist_ok=True)
60
+ elif zipfile.is_zipfile(path):
61
+ shutil.unpack_archive(path, plugin_path, format="zip")
62
+ else:
63
+ raise ValueError(
64
+ f"Plugin path '{path}' is not a valid zip file or a directory."
65
+ )
66
+
67
+ return plugin_path
68
+
69
+
70
+ def uninstall_plugin_files(plugin_id):
71
+ """
72
+ Uninstall plugin files.
73
+ """
74
+ plugin_path = Path(config.PLUGIN_FOLDER) / plugin_id
75
+ if plugin_path.exists():
76
+ shutil.rmtree(plugin_path)
77
+ return True
78
+ return False
79
+
80
+
81
+ def uninstall_plugin(plugin_id):
82
+ """
83
+ Uninstall a plugin.
84
+ """
85
+ installed = uninstall_plugin_files(plugin_id)
86
+ plugin = Plugin.query.filter_by(plugin_id=plugin_id).one_or_none()
87
+ if plugin is not None:
88
+ installed = True
89
+ plugin.delete()
90
+
91
+ if not installed:
92
+ raise ValueError(f"Plugin '{plugin_id}' is not installed.")
93
+ return True
94
+
95
+
96
+ def create_plugin_skeleton(
97
+ path,
98
+ id,
99
+ name,
100
+ description=None,
101
+ version=None,
102
+ maintainer=None,
103
+ website=None,
104
+ license=None,
105
+ force=False,
106
+ ):
107
+ plugin_template_path = (
108
+ Path(__file__).parent.parent.parent / "plugin_template"
109
+ )
110
+ plugin_path = Path(path) / id
111
+
112
+ if plugin_path.exists():
113
+ if force:
114
+ shutil.rmtree(plugin_path)
115
+ else:
116
+ raise FileExistsError(
117
+ f"Plugin '{id}' already exists in {plugin_path}."
118
+ )
119
+
120
+ shutil.copytree(plugin_template_path, plugin_path)
121
+
122
+ manifest = PluginManifest.from_file(plugin_path / "manifest.toml")
123
+
124
+ manifest.id = id
125
+ manifest.name = name
126
+ if description:
127
+ manifest.description = description
128
+ if version:
129
+ manifest.version = version
130
+ if maintainer:
131
+ manifest.maintainer = maintainer
132
+ if website:
133
+ manifest.website = website
134
+ if license:
135
+ manifest.license = license
136
+
137
+ manifest.validate()
138
+ manifest.write_to_path(plugin_path)
139
+
140
+ return plugin_path
141
+
142
+
143
+ def create_plugin_package(path, output_path, force=False):
144
+ """
145
+ Create a plugin package.
146
+ """
147
+ path = Path(path)
148
+ if not path.exists():
149
+ raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
150
+
151
+ manifest = PluginManifest.from_plugin_path(path)
152
+
153
+ output_path = Path(output_path)
154
+ if not output_path.suffix == ".zip":
155
+ output_path /= f"{manifest.id}-{manifest.version}.zip"
156
+ if output_path.exists():
157
+ if force:
158
+ output_path.unlink()
159
+ else:
160
+ raise FileExistsError(
161
+ f"Output path '{output_path}' already exists."
162
+ )
163
+
164
+ output_path = shutil.make_archive(
165
+ output_path.with_suffix(""),
166
+ "zip",
167
+ path,
168
+ )
169
+ return output_path
zou/app/utils/commands.py CHANGED
@@ -5,8 +5,10 @@ import datetime
5
5
  import tempfile
6
6
  import sys
7
7
  import shutil
8
+ import click
9
+ import orjson as json
8
10
 
9
-
11
+ from tabulate import tabulate
10
12
  from ldap3 import Server, Connection, ALL, NTLM, SIMPLE
11
13
  from zou.app.utils import thumbnail as thumbnail_utils, auth
12
14
  from zou.app.stores import auth_tokens_store, file_store, queue_store
@@ -27,6 +29,7 @@ from zou.app.services import (
27
29
  from zou.app.models.person import Person
28
30
  from zou.app.models.preview_file import PreviewFile
29
31
  from zou.app.models.task import Task
32
+ from zou.app.models.plugin import Plugin
30
33
  from sqlalchemy.sql.expression import not_
31
34
 
32
35
  from zou.app.services.exception import (
@@ -823,3 +826,50 @@ def renormalize_movie_preview_files(
823
826
  f"Renormalization of preview file {preview_file_id} failed: {e}"
824
827
  )
825
828
  continue
829
+
830
+
831
+ def list_plugins(output_format, verbose, filter_field, filter_value):
832
+ with app.app_context():
833
+ query = Plugin.query
834
+
835
+ # Apply filter if needed
836
+ if filter_field and filter_value:
837
+ if filter_field == "maintainer":
838
+ query = query.filter(
839
+ Plugin.maintainer_name.ilike(f"%{filter_value}%")
840
+ )
841
+ else:
842
+ model_field = getattr(Plugin, filter_field)
843
+ query = query.filter(model_field.ilike(f"%{filter_value}%"))
844
+
845
+ plugins = query.order_by(Plugin.name).all()
846
+
847
+ if not plugins:
848
+ click.echo("No plugins found matching the criteria.")
849
+ return
850
+
851
+ plugin_list = []
852
+ for plugin in plugins:
853
+ maintainer = (
854
+ f"{plugin.maintainer_name} <{plugin.maintainer_email}>"
855
+ if plugin.maintainer_email
856
+ else plugin.maintainer_name
857
+ )
858
+ plugin_data = {
859
+ "Plugin ID": plugin.plugin_id,
860
+ "Name": plugin.name,
861
+ "Version": plugin.version,
862
+ "Maintainer": maintainer,
863
+ "License": plugin.license,
864
+ }
865
+ if verbose:
866
+ plugin_data["Description"] = plugin.description or "-"
867
+ plugin_data["Website"] = plugin.website or "-"
868
+ plugin_list.append(plugin_data)
869
+
870
+ if output_format == "table":
871
+ headers = plugin_list[0].keys()
872
+ rows = [p.values() for p in plugin_list]
873
+ click.echo(tabulate(rows, headers, tablefmt="fancy_grid"))
874
+ elif output_format == "json":
875
+ click.echo(json.dumps(plugin_list, indent=2, ensure_ascii=False))
@@ -0,0 +1,88 @@
1
+ import tomlkit
2
+ import semver
3
+ import email.utils
4
+ import spdx_license_list
5
+ import zipfile
6
+
7
+ from pathlib import Path
8
+ from collections.abc import MutableMapping
9
+
10
+
11
+ class PluginManifest(MutableMapping):
12
+ def __init__(self, data):
13
+ super().__setattr__("data", data)
14
+ self.validate()
15
+
16
+ @classmethod
17
+ def from_plugin_path(cls, path):
18
+ path = Path(path)
19
+ if path.is_dir():
20
+ return cls.from_file(path / "manifest.toml")
21
+ elif zipfile.is_zipfile(path):
22
+ with zipfile.ZipFile(path) as z:
23
+ with z.open("manifest.toml") as f:
24
+ data = tomlkit.load(f)
25
+ return cls(data)
26
+ else:
27
+ raise ValueError(f"Invalid plugin path: {path}")
28
+
29
+ @classmethod
30
+ def from_file(cls, path):
31
+ with open(path, "rb") as f:
32
+ data = tomlkit.load(f)
33
+ return cls(data)
34
+
35
+ def write_to_path(self, path):
36
+ path = Path(path)
37
+ with open(path / "manifest.toml", "w", encoding="utf-8") as f:
38
+ tomlkit.dump(self.data, f)
39
+
40
+ def validate(self):
41
+ semver.Version.parse(str(self.data["version"]))
42
+ spdx_license_list.LICENSES[self.data["license"]]
43
+ if "maintainer" in self.data:
44
+ name, email_addr = email.utils.parseaddr(self.data["maintainer"])
45
+ self.data["maintainer_name"] = name
46
+ self.data["maintainer_email"] = email_addr
47
+
48
+ def to_model_dict(self):
49
+ return {
50
+ "plugin_id": self.data["id"],
51
+ "name": self.data["name"],
52
+ "description": self.data.get("description"),
53
+ "version": str(self.data["version"]),
54
+ "maintainer_name": self.data.get("maintainer_name"),
55
+ "maintainer_email": self.data.get("maintainer_email"),
56
+ "website": self.data.get("website"),
57
+ "license": self.data["license"],
58
+ }
59
+
60
+ def __getitem__(self, key):
61
+ return self.data[key]
62
+
63
+ def __setitem__(self, key, value):
64
+ self.data[key] = value
65
+
66
+ def __delitem__(self, key):
67
+ del self.data[key]
68
+
69
+ def __iter__(self):
70
+ return iter(self.data)
71
+
72
+ def __len__(self):
73
+ return len(self.data)
74
+
75
+ def __repr__(self):
76
+ return f"<PluginManifest {self.data!r}>"
77
+
78
+ def __getattr__(self, attr):
79
+ try:
80
+ return self.data[attr]
81
+ except KeyError:
82
+ raise AttributeError(f"'PluginManifest' has no attribute '{attr}'")
83
+
84
+ def __setattr__(self, attr, value):
85
+ if attr == "data":
86
+ super().__setattr__(attr, value)
87
+ else:
88
+ self.data[attr] = value
zou/cli.py CHANGED
@@ -9,12 +9,13 @@ import traceback
9
9
  from sqlalchemy.exc import IntegrityError
10
10
 
11
11
  from zou.app.utils import dbhelpers, auth, commands
12
- from zou.app.services import persons_service, auth_service, plugin_service
12
+ from zou.app.services import persons_service, auth_service, plugins_service
13
13
  from zou.app.services.exception import (
14
14
  IsUserLimitReachedException,
15
15
  PersonNotFoundException,
16
16
  TwoFactorAuthenticationNotEnabledException,
17
17
  )
18
+
18
19
  from zou.app import app, config
19
20
 
20
21
  from zou import __file__ as root_path
@@ -659,7 +660,7 @@ def install_plugin(path, force=False):
659
660
  Install a plugin.
660
661
  """
661
662
  with app.app_context():
662
- plugin_service.install_plugin(path, force)
663
+ plugins_service.install_plugin(path, force)
663
664
  print(f"Plugin {path} installed. Restart the server to apply changes.")
664
665
 
665
666
 
@@ -673,7 +674,7 @@ def uninstall_plugin(id):
673
674
  Uninstall a plugin.
674
675
  """
675
676
  with app.app_context():
676
- plugin_service.uninstall_plugin(id)
677
+ plugins_service.uninstall_plugin(id)
677
678
  print(f"Plugin {id} uninstalled.")
678
679
 
679
680
 
@@ -737,7 +738,7 @@ def create_plugin_skeleton(
737
738
  """
738
739
  Create a plugin skeleton.
739
740
  """
740
- plugin_path = plugin_service.create_plugin_skeleton(
741
+ plugin_path = plugins_service.create_plugin_skeleton(
741
742
  path,
742
743
  id,
743
744
  name,
@@ -751,5 +752,64 @@ def create_plugin_skeleton(
751
752
  print(f"Plugin skeleton created in '{plugin_path}'.")
752
753
 
753
754
 
755
+ @cli.command()
756
+ @click.option(
757
+ "--path",
758
+ required=True,
759
+ )
760
+ @click.option(
761
+ "--output-path",
762
+ required=True,
763
+ )
764
+ @click.option(
765
+ "--force",
766
+ is_flag=True,
767
+ default=False,
768
+ show_default=True,
769
+ )
770
+ def create_plugin_package(
771
+ path,
772
+ output_path,
773
+ force=False,
774
+ ):
775
+ """
776
+ Create a plugin package.
777
+ """
778
+ plugin_path = plugins_service.create_plugin_package(
779
+ path, output_path, force
780
+ )
781
+ print(f"Plugin package created in '{plugin_path}'.")
782
+
783
+
784
+ @cli.command()
785
+ @click.option(
786
+ "--format",
787
+ "output_format",
788
+ type=click.Choice(["table", "json"], case_sensitive=False),
789
+ default="table",
790
+ show_default=True,
791
+ help="Output format: table or json.",
792
+ )
793
+ @click.option(
794
+ "--verbose",
795
+ is_flag=True,
796
+ default=False,
797
+ help="Show more plugin information.",
798
+ )
799
+ @click.option(
800
+ "--filter-field",
801
+ type=click.Choice(
802
+ ["plugin_id", "name", "maintainer", "license"], case_sensitive=False
803
+ ),
804
+ help="Field to filter by.",
805
+ )
806
+ @click.option("--filter-value", type=str, help="Value to search in the field.")
807
+ def list_plugins(output_format, verbose, filter_field, filter_value):
808
+ """
809
+ List installed plugins.
810
+ """
811
+ commands.list_plugins(output_format, verbose, filter_field, filter_value)
812
+
813
+
754
814
  if __name__ == "__main__":
755
815
  cli()
zou/event_stream.py CHANGED
@@ -27,6 +27,7 @@ def _get_empty_room(current_frame=0):
27
27
  return {
28
28
  "playlist_id": None,
29
29
  "user_id": None,
30
+ "local_id": None,
30
31
  "people": [],
31
32
  "is_playing": False,
32
33
  "current_entity_id": None,
@@ -81,6 +82,7 @@ def _emit_people_updated(room_id, people):
81
82
  def _update_room_playing_status(data, room):
82
83
  room["playlist_id"] = data.get("playlist_id", False)
83
84
  room["user_id"] = data.get("user_id", False)
85
+ room["local_id"] = data.get("local_id", False)
84
86
  room["is_playing"] = data.get("is_playing", False)
85
87
  room["is_repeating"] = data.get("is_repeating", False)
86
88
  room["is_laser_mode"] = data.get("is_laser_mode", False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zou
3
- Version: 0.20.39
3
+ Version: 0.20.41
4
4
  Summary: API to store and manage the data of your animation production
5
5
  Home-page: https://zou.cg-wire.com
6
6
  Author: CG Wire
@@ -55,10 +55,10 @@ Requires-Dist: numpy==2.2.5; python_version >= "3.10"
55
55
  Requires-Dist: opencv-python==4.11.0.86
56
56
  Requires-Dist: OpenTimelineIO==0.17.0
57
57
  Requires-Dist: OpenTimelineIO-Plugins==0.17.0
58
- Requires-Dist: orjson==3.10.16
58
+ Requires-Dist: orjson==3.10.18
59
59
  Requires-Dist: pillow==11.2.1
60
60
  Requires-Dist: psutil==7.0.0
61
- Requires-Dist: psycopg[binary]==3.2.6
61
+ Requires-Dist: psycopg[binary]==3.2.7
62
62
  Requires-Dist: pyotp==2.9.0
63
63
  Requires-Dist: pysaml2==7.5.2
64
64
  Requires-Dist: python-nomad==2.1.0
@@ -73,6 +73,7 @@ Requires-Dist: slackclient==2.9.4
73
73
  Requires-Dist: spdx-license-list==3.26.0
74
74
  Requires-Dist: sqlalchemy_utils==0.41.2
75
75
  Requires-Dist: sqlalchemy==2.0.40
76
+ Requires-Dist: tabulate==0.9.0
76
77
  Requires-Dist: tomlkit==0.13.2
77
78
  Requires-Dist: ua-parser==1.0.1
78
79
  Requires-Dist: werkzeug==3.1.3
@@ -1,10 +1,10 @@
1
- zou/__init__.py,sha256=-Y9d7FsYuoz4vluQDPNW8WBU0X-44kkaC8c3Vji_UHw,24
2
- zou/cli.py,sha256=lUOs9pcaQmT9Hr2XHIZk8a5IKyuW9b_VkJ4FHaAbI_Y,20787
1
+ zou/__init__.py,sha256=6x53NYL-6OvGfQIBQiG1cJ29THw9_vSWG-_ZcXlVWpg,24
2
+ zou/cli.py,sha256=Mes4MNjmPFxrAMHkiJtmU4G3F9VMvFJfieOAyt2Irb8,22064
3
3
  zou/debug.py,sha256=1fawPbkD4wn0Y9Gk0BiBFSa-CQe5agFi8R9uJYl2Uyk,520
4
- zou/event_stream.py,sha256=DTn3v9jDw3KrR68k9jAAesJ5QGs-9j565FitM9RSkb0,8214
4
+ zou/event_stream.py,sha256=yTU1Z3r55SiYm8Y5twtJIo5kTnhbBK-XKc8apdgvzNw,8291
5
5
  zou/job_settings.py,sha256=_aqBhujt2Q8sXRWIbgbDf-LUdXRdBimdtTc-fZbiXoY,202
6
6
  zou/app/__init__.py,sha256=zGmaBGBHSS_Px34I3_WZmcse62G_AZJArjm4F6TwmRk,7100
7
- zou/app/api.py,sha256=5a3lcBnVxmQiUp6_jwWQN2N2h8rLm9Cg96xI2PtP6Ik,4857
7
+ zou/app/api.py,sha256=TYWUC__i23Lbpd2ahfJ_0Ny0fGieAluT5EP_wd9cQKw,5033
8
8
  zou/app/config.py,sha256=eXVrmZf550Tk0fiN0Asfrhel0StesTDLTAYU6LdM3n4,6747
9
9
  zou/app/mixin.py,sha256=MGRrwLLRjWQtXHZ1YTaMgR5Jc8khnOrFqkvy2hzP5QY,5211
10
10
  zou/app/swagger.py,sha256=Jr7zsMqJi0V4FledODOdu-aqqVE02jMFzhqVxHK0_2c,54158
@@ -21,7 +21,7 @@ zou/app/blueprints/comments/__init__.py,sha256=WqpJ7-_dK1cInGTFJAxQ7syZtPCotwq2o
21
21
  zou/app/blueprints/comments/resources.py,sha256=hS5Yt8Mz7d9e19A4-yXaXO12sFugg_UzLPBxKXdtQYU,19260
22
22
  zou/app/blueprints/concepts/__init__.py,sha256=sP_P4mfYvfMcgeE6MHZYP3eD0Lz0Lwit5-CFuVnA-Jg,894
23
23
  zou/app/blueprints/concepts/resources.py,sha256=maJNrBAWX0bKbDKtOZc3YFp4nTVtIdkkAA4H9WA9n1Y,10140
24
- zou/app/blueprints/crud/__init__.py,sha256=3hLp3NTFSliDNbBFTEp3QOgcunLQvtn2fAEVUCxyvBs,8580
24
+ zou/app/blueprints/crud/__init__.py,sha256=HWBVCcaGm87SGK67LoR-WkQtqdUU-XogiDRNLpLXfyE,8749
25
25
  zou/app/blueprints/crud/asset_instance.py,sha256=va3mw79aPKry2m9PYAmjVePTScigewDjwD1c672f0y0,1335
26
26
  zou/app/blueprints/crud/attachment_file.py,sha256=-yur0V16BOTvpdqtNymDTHEugwRPgGtWccdXotpvYZ4,1193
27
27
  zou/app/blueprints/crud/base.py,sha256=HJcZKeNe3RVe_qEC9bSlpz4FRKhqavzrsfFLSZ8OmoY,15907
@@ -211,7 +211,7 @@ zou/app/services/news_service.py,sha256=eOXkvLhOcgncI2NrgiJEccV28oxZX5CsZVqaE-l4
211
211
  zou/app/services/notifications_service.py,sha256=7GDRio_mGaRYV5BHOAdpxBZjA_LLYUfVpbwZqy1n9pI,15685
212
212
  zou/app/services/persons_service.py,sha256=HjV-su80Y2BO9l5zoBKHMNF0mDGtkWqPhEOs3nQ3nlI,16566
213
213
  zou/app/services/playlists_service.py,sha256=OCq6CD9XSzH99Eipie8gyEBo8BAGQp2wEMbYKqHS9vw,32496
214
- zou/app/services/plugin_service.py,sha256=Xyy6sbZiKcQHdLDRPupBh4pWFye1DP3nAY4Kdor6bEE,5685
214
+ zou/app/services/plugins_service.py,sha256=SW0lTQCLJTT-Gxi8pEAmtyMOF97ebOyTHUBYqZ36c88,4422
215
215
  zou/app/services/preview_files_service.py,sha256=Yk-vwzHuKTzNkEZfl9DhQRdDuRU006uwZxJ-RKajEkI,35842
216
216
  zou/app/services/projects_service.py,sha256=aIbYaFomy7OX2Pxvkf9w5qauDvkjuc9ummSGNYIpQMY,21249
217
217
  zou/app/services/scenes_service.py,sha256=iXN19HU4njPF5VtZXuUrVJ-W23ZQuQNPC3ADXltbWtU,992
@@ -235,7 +235,7 @@ zou/app/utils/auth.py,sha256=DZfZSr1Ulge0UK3hfvOWsMo3_d7RVP_llV118u9BtUI,870
235
235
  zou/app/utils/cache.py,sha256=MRluTvGG67ybOkyzgD70B6PGKMdRyFdTc0AYy3dEQe8,1210
236
236
  zou/app/utils/chats.py,sha256=ORngxQ3IQQF0QcVFJLxJ-RaU4ksQ9-0M8cmPa0pc0Ho,4302
237
237
  zou/app/utils/colors.py,sha256=LaGV17NL_8xY0XSp8snGWz5UMwGnm0KPWXyE5BTMG6w,200
238
- zou/app/utils/commands.py,sha256=7PeiQ9YFeta4onlsho4Cabjt34SUkmy1w3KW0v_gphs,27951
238
+ zou/app/utils/commands.py,sha256=rd93hTuF-kjmIVBnpBL1uOPDfwY10wJIyu9DQKkbbNM,29785
239
239
  zou/app/utils/csv_utils.py,sha256=GiI8SeUqmIh9o1JwhZGkQXU_0K0EcPrRHYIZ8bMoYzk,1228
240
240
  zou/app/utils/date_helpers.py,sha256=jFxDPCbAasg0I1gsC72AKEbGcx5c4pLqXZkSfZ4wLdQ,4724
241
241
  zou/app/utils/dbhelpers.py,sha256=RSJuoxLexGJyME16GQCs-euFLBR0u-XAFdJ1KMSv5M8,1143
@@ -250,6 +250,7 @@ zou/app/utils/git.py,sha256=MhmAYvEY-bXsnLvcHUW_NY5V636lJL3H-cdNrTHgLGk,114
250
250
  zou/app/utils/logs.py,sha256=lB6kyFmeANxCILUULLqGN8fuq9IY5FcbrVWmLdqWs2U,1404
251
251
  zou/app/utils/monitoring.py,sha256=XCpl0QshKD_tSok1vrIu9lB97JsvKLhvxJo3UyeqEoQ,2153
252
252
  zou/app/utils/permissions.py,sha256=Oq91C_lN6aGVCtCVUqQhijMQEjXOiMezbngpjybzzQk,3426
253
+ zou/app/utils/plugins.py,sha256=wlyG3GcbDYSlMXdKCk6u2gE_5nD5ZtDoQGvEjtt3LKg,2611
253
254
  zou/app/utils/query.py,sha256=q8ETGPAqnz0Pt9xWoQt5o7FFAVYUKVCJiWpwefIr-iU,4592
254
255
  zou/app/utils/redis.py,sha256=xXEh9pl-3qPbr89dKHvcXSUTC6hd77vv_N8PVcRRZTE,377
255
256
  zou/app/utils/remote_job.py,sha256=QPxcCWEv-NM1Q4IQawAyJAiSORwkMeOlByQb9OCShEw,2522
@@ -439,9 +440,9 @@ zou/remote/normalize_movie.py,sha256=zNfEY3N1UbAHZfddGONTg2Sff3ieLVWd4dfZa1dpnes
439
440
  zou/remote/playlist.py,sha256=AsDo0bgYhDcd6DfNRV6r6Jj3URWwavE2ZN3VkKRPbLU,3293
440
441
  zou/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
441
442
  zou/utils/movie.py,sha256=d67fIL9dVBKt-E_qCGXRbNNdbJaJR5sHvZeX3hf8ldE,16559
442
- zou-0.20.39.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
443
- zou-0.20.39.dist-info/METADATA,sha256=wbPTR4AkC8qoFwPd34cf5Sushv7wehW-M194G0ATwWw,6795
444
- zou-0.20.39.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
445
- zou-0.20.39.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
446
- zou-0.20.39.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
447
- zou-0.20.39.dist-info/RECORD,,
443
+ zou-0.20.41.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
444
+ zou-0.20.41.dist-info/METADATA,sha256=DoA4xOe1_FffMRHTmIevzjGTihzRQKtcA21uDA75DWo,6826
445
+ zou-0.20.41.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
446
+ zou-0.20.41.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
447
+ zou-0.20.41.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
448
+ zou-0.20.41.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,195 +0,0 @@
1
- import zipfile
2
- import semver
3
- import spdx_license_list
4
- import shutil
5
- import email.utils
6
- import tomlkit
7
-
8
- from pathlib import Path
9
-
10
- from zou.app import config, db
11
- from zou.app.models.plugin import Plugin
12
-
13
-
14
- def install_plugin(path, force=False):
15
- """
16
- Install a plugin.
17
- """
18
- path = Path(path)
19
- if not path.exists():
20
- raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
21
-
22
- try:
23
- manifest_file = None
24
- if path.is_dir():
25
- manifest_file = open(path.joinpath("manifest.toml"), "rb")
26
- elif zipfile.is_zipfile(path):
27
- with zipfile.ZipFile(path) as zip_file:
28
- manifest_file = zip_file.open("manifest.toml", "rb")
29
- else:
30
- raise ValueError(
31
- f"Plugin path '{path}' is not a valid zip file or a directory."
32
- )
33
-
34
- # Read the plugin metadatas
35
- manifest = tomlkit.load(manifest_file)
36
- finally:
37
- if manifest_file is not None:
38
- manifest_file.close()
39
-
40
- version = str(semver.Version.parse(manifest["version"]))
41
- spdx_license_list.LICENSES[manifest["license"]]
42
- if manifest.get("maintainer") is not None:
43
- manifest["maintainer_name"], manifest["maintainer_email"] = (
44
- email.utils.parseaddr(manifest["maintainer"])
45
- )
46
-
47
- new_plugin_info = {
48
- "plugin_id": manifest["id"],
49
- "name": manifest["name"],
50
- "description": manifest.get("description"),
51
- "version": version,
52
- "maintainer_name": manifest["maintainer_name"],
53
- "maintainer_email": manifest.get("maintainer_email"),
54
- "website": manifest.get("website"),
55
- "license": manifest["license"],
56
- }
57
-
58
- # Check if the plugin is already installed
59
- plugin = Plugin.query.filter_by(
60
- plugin_id=new_plugin_info["plugin_id"]
61
- ).one_or_none()
62
-
63
- already_installed = False
64
- try:
65
- if plugin is not None:
66
- existing_plugin_version = semver.Version.parse(plugin.version)
67
-
68
- if not force:
69
- if existing_plugin_version == version:
70
- raise ValueError(
71
- f"Plugin '{manifest['name']}' version {version} is already installed."
72
- )
73
- elif existing_plugin_version > version:
74
- raise ValueError(
75
- f"Plugin '{manifest['name']}' version {version} is older than the installed version {existing_plugin_version}."
76
- )
77
- already_installed = True
78
- plugin.update_no_commit(new_plugin_info)
79
- else:
80
- plugin = Plugin.create_no_commit(**new_plugin_info)
81
-
82
- install_plugin_files(
83
- new_plugin_info["plugin_id"],
84
- path,
85
- already_installed=already_installed,
86
- )
87
- except:
88
- db.session.rollback()
89
- db.session.remove()
90
- raise
91
- Plugin.commit()
92
-
93
- return plugin.serialize()
94
-
95
-
96
- def install_plugin_files(plugin_id, path, already_installed=False):
97
- """
98
- Install the plugin files.
99
- """
100
- path = Path(path)
101
- plugin_path = Path(config.PLUGIN_FOLDER).joinpath(plugin_id)
102
- if already_installed:
103
- shutil.rmtree(plugin_path)
104
-
105
- plugin_path.mkdir(parents=True, exist_ok=True)
106
-
107
- if path.is_dir():
108
- shutil.copytree(path, plugin_path, dirs_exist_ok=True)
109
- elif zipfile.is_zipfile(path):
110
- shutil.unpack_archive(path, plugin_path, format="zip")
111
- else:
112
- raise ValueError(
113
- f"Plugin path '{path}' is not a valid zip file or a directory."
114
- )
115
-
116
- return plugin_path
117
-
118
-
119
- def uninstall_plugin_files(plugin_id):
120
- """
121
- Uninstall the plugin files.
122
- """
123
- plugin_path = Path(config.PLUGIN_FOLDER).joinpath(plugin_id)
124
- if plugin_path.exists():
125
- shutil.rmtree(plugin_path)
126
- return True
127
- return False
128
-
129
-
130
- def uninstall_plugin(plugin_id):
131
- """
132
- Uninstall a plugin.
133
- """
134
- installed = uninstall_plugin_files(plugin_id)
135
- plugin = Plugin.query.filter_by(plugin_id=plugin_id).one_or_none()
136
- if plugin is not None:
137
- installed = True
138
- plugin.delete()
139
-
140
- if not installed:
141
- raise ValueError(f"Plugin '{plugin_id}' is not installed.")
142
- return True
143
-
144
-
145
- def create_plugin_skeleton(
146
- path,
147
- id,
148
- name,
149
- description=None,
150
- version=None,
151
- maintainer=None,
152
- website=None,
153
- license=None,
154
- force=False,
155
- ):
156
- """
157
- Create a plugin skeleton.
158
- """
159
- plugin_template_path = Path(__file__).parent.parent.parent.joinpath(
160
- "plugin_template"
161
- )
162
- plugin_path = Path(path).joinpath(id)
163
- if plugin_path.exists():
164
- if force:
165
- shutil.rmtree(plugin_path)
166
- else:
167
- raise ValueError(f"Plugin '{id}' already exists in {plugin_path}.")
168
-
169
- shutil.copytree(plugin_template_path, plugin_path)
170
-
171
- manifest_path = plugin_path.joinpath("manifest.toml")
172
- with open(manifest_path, "r") as manifest_file:
173
- manifest = tomlkit.load(manifest_file)
174
-
175
- manifest["id"] = id
176
- if name is not None:
177
- manifest["name"] = name
178
- if description is not None:
179
- manifest["description"] = description
180
- if version is not None:
181
- manifest["version"] = version
182
- semver.Version.parse(manifest["version"])
183
- if maintainer is not None:
184
- manifest["maintainer"] = maintainer
185
- email.utils.parseaddr(manifest["maintainer"])
186
- if website is not None:
187
- manifest["website"] = website
188
- if license is not None:
189
- manifest["license"] = license
190
- spdx_license_list.LICENSES[manifest["license"]]
191
-
192
- with open(manifest_path, "w") as manifest_file:
193
- tomlkit.dump(manifest, manifest_file)
194
-
195
- return plugin_path