zou 0.20.39__py3-none-any.whl → 0.20.40__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 +1 -1
- zou/app/api.py +18 -14
- zou/app/blueprints/crud/__init__.py +4 -0
- zou/app/services/plugins_service.py +169 -0
- zou/app/utils/commands.py +51 -1
- zou/app/utils/plugins.py +88 -0
- zou/cli.py +64 -4
- {zou-0.20.39.dist-info → zou-0.20.40.dist-info}/METADATA +2 -1
- {zou-0.20.39.dist-info → zou-0.20.40.dist-info}/RECORD +13 -12
- zou/app/services/plugin_service.py +0 -195
- {zou-0.20.39.dist-info → zou-0.20.40.dist-info}/WHEEL +0 -0
- {zou-0.20.39.dist-info → zou-0.20.40.dist-info}/entry_points.txt +0 -0
- {zou-0.20.39.dist-info → zou-0.20.40.dist-info}/licenses/LICENSE +0 -0
- {zou-0.20.39.dist-info → zou-0.20.40.dist-info}/top_level.txt +0 -0
zou/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.20.
|
|
1
|
+
__version__ = "0.20.40"
|
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
|
-
|
|
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(
|
|
106
|
-
for plugin_id in os.listdir(
|
|
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}
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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))
|
zou/app/utils/plugins.py
ADDED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zou
|
|
3
|
-
Version: 0.20.
|
|
3
|
+
Version: 0.20.40
|
|
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
|
|
@@ -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
|
|
2
|
-
zou/cli.py,sha256=
|
|
1
|
+
zou/__init__.py,sha256=LX2y5NhPizX5sFF0x5gfQ8yRTDJdVoERlxtXJcFvRd4,24
|
|
2
|
+
zou/cli.py,sha256=Mes4MNjmPFxrAMHkiJtmU4G3F9VMvFJfieOAyt2Irb8,22064
|
|
3
3
|
zou/debug.py,sha256=1fawPbkD4wn0Y9Gk0BiBFSa-CQe5agFi8R9uJYl2Uyk,520
|
|
4
4
|
zou/event_stream.py,sha256=DTn3v9jDw3KrR68k9jAAesJ5QGs-9j565FitM9RSkb0,8214
|
|
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=
|
|
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=
|
|
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/
|
|
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=
|
|
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.
|
|
443
|
-
zou-0.20.
|
|
444
|
-
zou-0.20.
|
|
445
|
-
zou-0.20.
|
|
446
|
-
zou-0.20.
|
|
447
|
-
zou-0.20.
|
|
443
|
+
zou-0.20.40.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
|
444
|
+
zou-0.20.40.dist-info/METADATA,sha256=B0-k7wF7MXU4PPR2r1VPIuDaexT5PSBKmzosFULQ9fA,6826
|
|
445
|
+
zou-0.20.40.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
|
|
446
|
+
zou-0.20.40.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
|
|
447
|
+
zou-0.20.40.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
|
|
448
|
+
zou-0.20.40.dist-info/RECORD,,
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|