zou 0.20.38__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 +38 -43
- zou/app/blueprints/crud/__init__.py +10 -0
- zou/app/blueprints/crud/budget.py +21 -0
- zou/app/blueprints/crud/budget_entry.py +15 -0
- zou/app/blueprints/crud/plugin.py +13 -0
- zou/app/blueprints/crud/salary_scale.py +73 -0
- zou/app/blueprints/playlists/resources.py +15 -0
- zou/app/blueprints/projects/__init__.py +17 -0
- zou/app/blueprints/projects/resources.py +402 -7
- zou/app/blueprints/shots/resources.py +1 -0
- zou/app/mixin.py +12 -1
- zou/app/models/budget.py +39 -0
- zou/app/models/budget_entry.py +65 -0
- zou/app/models/person.py +17 -1
- zou/app/models/plugin.py +21 -0
- zou/app/models/salary_scale.py +28 -0
- zou/app/services/budget_service.py +195 -0
- zou/app/services/comments_service.py +1 -1
- zou/app/services/exception.py +8 -0
- zou/app/services/plugins_service.py +169 -0
- zou/app/services/user_service.py +1 -3
- zou/app/utils/commands.py +51 -1
- zou/app/utils/fields.py +10 -0
- zou/app/utils/plugins.py +88 -0
- zou/cli.py +169 -1
- zou/event_stream.py +23 -5
- zou/migrations/versions/2762a797f1f9_add_people_salary_information.py +52 -0
- zou/migrations/versions/45f739ef962a_add_people_salary_scale_table.py +70 -0
- zou/migrations/versions/4aab1f84ad72_introduce_plugin_table.py +68 -0
- zou/migrations/versions/7a16258f2fab_add_currency_field_to_budgets.py +33 -0
- zou/migrations/versions/83e2f33a9b14_add_project_bugdet_table.py +57 -0
- zou/migrations/versions/8ab98c178903_add_budget_entry_table.py +123 -0
- zou/migrations/versions/d25118cddcaa_modify_salary_scale_model.py +133 -0
- zou/plugin_template/__init__.py +39 -0
- zou/plugin_template/routes.py +6 -0
- {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/METADATA +7 -3
- {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/RECORD +42 -22
- {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/WHEEL +1 -1
- {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/entry_points.txt +0 -0
- {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/licenses/LICENSE +0 -0
- {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from zou.app.models.budget import Budget
|
|
2
|
+
from zou.app.models.budget_entry import BudgetEntry
|
|
3
|
+
|
|
4
|
+
from zou.app.services.exception import (
|
|
5
|
+
BudgetNotFoundException,
|
|
6
|
+
BudgetEntryNotFoundException,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from zou.app.utils import events
|
|
10
|
+
from zou.app.utils import fields
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.exc import StatementError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_budget_raw(budget_id):
|
|
16
|
+
"""
|
|
17
|
+
Return budget corresponding to given budget ID.
|
|
18
|
+
"""
|
|
19
|
+
if budget_id is None:
|
|
20
|
+
raise BudgetNotFoundException()
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
budget = Budget.get(budget_id)
|
|
24
|
+
except StatementError:
|
|
25
|
+
raise BudgetNotFoundException()
|
|
26
|
+
|
|
27
|
+
if budget is None:
|
|
28
|
+
raise BudgetNotFoundException()
|
|
29
|
+
return budget
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_budget(budget_id):
|
|
33
|
+
"""
|
|
34
|
+
Return budget corresponding to given budget ID as a dictionary.
|
|
35
|
+
"""
|
|
36
|
+
return get_budget_raw(budget_id).serialize(relations=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_budgets(project_id):
|
|
40
|
+
"""
|
|
41
|
+
Return all budgets for given project ID.
|
|
42
|
+
"""
|
|
43
|
+
budgets = Budget.get_all_by(project_id=project_id)
|
|
44
|
+
return fields.present_models(budgets)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_budget(project_id, name, currency=None):
|
|
48
|
+
"""
|
|
49
|
+
Create a new budget for given project ID.
|
|
50
|
+
"""
|
|
51
|
+
last_budget = (
|
|
52
|
+
Budget.query.filter_by(project_id=project_id)
|
|
53
|
+
.order_by(Budget.revision.desc())
|
|
54
|
+
.first()
|
|
55
|
+
)
|
|
56
|
+
last_revision = 1
|
|
57
|
+
if last_budget is not None:
|
|
58
|
+
last_revision = last_budget.revision + 1
|
|
59
|
+
budget = Budget(
|
|
60
|
+
project_id=project_id,
|
|
61
|
+
name=name,
|
|
62
|
+
currency=currency,
|
|
63
|
+
revision=last_revision,
|
|
64
|
+
)
|
|
65
|
+
budget.save()
|
|
66
|
+
events.emit(
|
|
67
|
+
"budget:create",
|
|
68
|
+
{"budget_id": str(budget.id)},
|
|
69
|
+
project_id=project_id,
|
|
70
|
+
)
|
|
71
|
+
return budget.serialize()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def update_budget(budget_id, name=None, currency=None):
|
|
75
|
+
"""
|
|
76
|
+
Update budget corresponding to given budget ID.
|
|
77
|
+
"""
|
|
78
|
+
budget = get_budget_raw(budget_id)
|
|
79
|
+
if name is not None:
|
|
80
|
+
budget.name = name
|
|
81
|
+
if currency is not None:
|
|
82
|
+
budget.currency = currency
|
|
83
|
+
budget.save()
|
|
84
|
+
events.emit(
|
|
85
|
+
"budget:update",
|
|
86
|
+
{"budget_id": str(budget.id)},
|
|
87
|
+
project_id=str(budget.project_id),
|
|
88
|
+
)
|
|
89
|
+
return budget.serialize()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def delete_budget(budget_id):
|
|
93
|
+
"""
|
|
94
|
+
Delete budget corresponding to given budget ID.
|
|
95
|
+
"""
|
|
96
|
+
budget = get_budget_raw(budget_id)
|
|
97
|
+
budget_entries = BudgetEntry.delete_all_by(budget_id=budget_id)
|
|
98
|
+
budget.delete()
|
|
99
|
+
events.emit(
|
|
100
|
+
"budget:delete",
|
|
101
|
+
{"budget_id": budget_id},
|
|
102
|
+
project_id=str(budget.project_id),
|
|
103
|
+
)
|
|
104
|
+
return budget.serialize()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_budget_entries(budget_id):
|
|
108
|
+
"""
|
|
109
|
+
Return all budget entries for given budget ID.
|
|
110
|
+
"""
|
|
111
|
+
budget_entries = BudgetEntry.get_all_by(budget_id=budget_id)
|
|
112
|
+
return fields.present_models(budget_entries)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_budget_entry_raw(budget_entry_id):
|
|
116
|
+
"""
|
|
117
|
+
Return budget entry corresponding to given budget entry ID.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
budget_entry = BudgetEntry.get(budget_entry_id)
|
|
121
|
+
except StatementError:
|
|
122
|
+
raise BudgetEntryNotFoundException()
|
|
123
|
+
|
|
124
|
+
if budget_entry is None:
|
|
125
|
+
raise BudgetEntryNotFoundException()
|
|
126
|
+
return budget_entry
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_budget_entry(budget_entry_id):
|
|
130
|
+
"""
|
|
131
|
+
Return budget entry corresponding to given budget entry ID as a dictionary.
|
|
132
|
+
"""
|
|
133
|
+
return get_budget_entry_raw(budget_entry_id).serialize()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def create_budget_entry(
|
|
137
|
+
budget_id,
|
|
138
|
+
department_id,
|
|
139
|
+
start_date,
|
|
140
|
+
months_duration,
|
|
141
|
+
daily_salary,
|
|
142
|
+
position,
|
|
143
|
+
seniority,
|
|
144
|
+
person_id=None,
|
|
145
|
+
):
|
|
146
|
+
"""
|
|
147
|
+
Create a new budget entry for given budget ID.
|
|
148
|
+
"""
|
|
149
|
+
budget = get_budget_raw(budget_id)
|
|
150
|
+
budget_entry = BudgetEntry.create(
|
|
151
|
+
budget_id=budget_id,
|
|
152
|
+
department_id=department_id,
|
|
153
|
+
person_id=person_id,
|
|
154
|
+
start_date=start_date,
|
|
155
|
+
months_duration=months_duration,
|
|
156
|
+
daily_salary=daily_salary,
|
|
157
|
+
position=position,
|
|
158
|
+
seniority=seniority,
|
|
159
|
+
)
|
|
160
|
+
events.emit(
|
|
161
|
+
"budget-entry:create",
|
|
162
|
+
{"budget_id": str(budget_id), "budget_entry_id": str(budget_entry.id)},
|
|
163
|
+
project_id=str(budget.project_id),
|
|
164
|
+
)
|
|
165
|
+
return budget_entry.serialize()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def update_budget_entry(budget_entry_id, data):
|
|
169
|
+
"""
|
|
170
|
+
Update budget entry corresponding to given budget entry ID.
|
|
171
|
+
"""
|
|
172
|
+
budget_entry = get_budget_entry_raw(budget_entry_id)
|
|
173
|
+
budget = get_budget_raw(str(budget_entry.budget_id))
|
|
174
|
+
budget_entry.update(data)
|
|
175
|
+
events.emit(
|
|
176
|
+
"budget-entry:update",
|
|
177
|
+
{"budget_id": str(budget.id), "budget_entry_id": str(budget_entry.id)},
|
|
178
|
+
project_id=str(budget.project_id),
|
|
179
|
+
)
|
|
180
|
+
return budget_entry.serialize()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def delete_budget_entry(budget_entry_id):
|
|
184
|
+
"""
|
|
185
|
+
Delete budget entry corresponding to given budget entry ID.
|
|
186
|
+
"""
|
|
187
|
+
budget_entry = get_budget_entry_raw(budget_entry_id)
|
|
188
|
+
budget = get_budget_raw(str(budget_entry.budget_id))
|
|
189
|
+
budget_entry.delete()
|
|
190
|
+
events.emit(
|
|
191
|
+
"budget-entry:delete",
|
|
192
|
+
{"budget_id": str(budget.id), "budget_entry_id": budget_entry_id},
|
|
193
|
+
project_id=str(budget.project_id),
|
|
194
|
+
)
|
|
195
|
+
return budget_entry.serialize()
|
zou/app/services/exception.py
CHANGED
|
@@ -149,6 +149,14 @@ class MetadataDescriptorNotFoundException(NotFound):
|
|
|
149
149
|
pass
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
class BudgetNotFoundException(NotFound):
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class BudgetEntryNotFoundException(NotFound):
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
152
160
|
class MalformedFileTreeException(Exception):
|
|
153
161
|
pass
|
|
154
162
|
|
|
@@ -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/services/user_service.py
CHANGED
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/fields.py
CHANGED
|
@@ -92,6 +92,13 @@ def serialize_models(models, relations=False, milliseconds=False):
|
|
|
92
92
|
]
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def present_models(models):
|
|
96
|
+
"""
|
|
97
|
+
Present a list of models (useful for json dumping)
|
|
98
|
+
"""
|
|
99
|
+
return [model.present() for model in models if model is not None]
|
|
100
|
+
|
|
101
|
+
|
|
95
102
|
def gen_uuid():
|
|
96
103
|
"""
|
|
97
104
|
Generate a unique identifier (useful for json dumping).
|
|
@@ -123,6 +130,9 @@ def get_default_date_object(date_string):
|
|
|
123
130
|
|
|
124
131
|
|
|
125
132
|
def is_valid_id(uuid):
|
|
133
|
+
"""
|
|
134
|
+
Check if a given string is a valid UUID.
|
|
135
|
+
"""
|
|
126
136
|
_UUID_RE = re.compile(
|
|
127
137
|
"([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}){1}"
|
|
128
138
|
)
|
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
|