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.
Files changed (42) hide show
  1. zou/__init__.py +1 -1
  2. zou/app/api.py +38 -43
  3. zou/app/blueprints/crud/__init__.py +10 -0
  4. zou/app/blueprints/crud/budget.py +21 -0
  5. zou/app/blueprints/crud/budget_entry.py +15 -0
  6. zou/app/blueprints/crud/plugin.py +13 -0
  7. zou/app/blueprints/crud/salary_scale.py +73 -0
  8. zou/app/blueprints/playlists/resources.py +15 -0
  9. zou/app/blueprints/projects/__init__.py +17 -0
  10. zou/app/blueprints/projects/resources.py +402 -7
  11. zou/app/blueprints/shots/resources.py +1 -0
  12. zou/app/mixin.py +12 -1
  13. zou/app/models/budget.py +39 -0
  14. zou/app/models/budget_entry.py +65 -0
  15. zou/app/models/person.py +17 -1
  16. zou/app/models/plugin.py +21 -0
  17. zou/app/models/salary_scale.py +28 -0
  18. zou/app/services/budget_service.py +195 -0
  19. zou/app/services/comments_service.py +1 -1
  20. zou/app/services/exception.py +8 -0
  21. zou/app/services/plugins_service.py +169 -0
  22. zou/app/services/user_service.py +1 -3
  23. zou/app/utils/commands.py +51 -1
  24. zou/app/utils/fields.py +10 -0
  25. zou/app/utils/plugins.py +88 -0
  26. zou/cli.py +169 -1
  27. zou/event_stream.py +23 -5
  28. zou/migrations/versions/2762a797f1f9_add_people_salary_information.py +52 -0
  29. zou/migrations/versions/45f739ef962a_add_people_salary_scale_table.py +70 -0
  30. zou/migrations/versions/4aab1f84ad72_introduce_plugin_table.py +68 -0
  31. zou/migrations/versions/7a16258f2fab_add_currency_field_to_budgets.py +33 -0
  32. zou/migrations/versions/83e2f33a9b14_add_project_bugdet_table.py +57 -0
  33. zou/migrations/versions/8ab98c178903_add_budget_entry_table.py +123 -0
  34. zou/migrations/versions/d25118cddcaa_modify_salary_scale_model.py +133 -0
  35. zou/plugin_template/__init__.py +39 -0
  36. zou/plugin_template/routes.py +6 -0
  37. {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/METADATA +7 -3
  38. {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/RECORD +42 -22
  39. {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/WHEEL +1 -1
  40. {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/entry_points.txt +0 -0
  41. {zou-0.20.38.dist-info → zou-0.20.40.dist-info}/licenses/LICENSE +0 -0
  42. {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()
@@ -333,7 +333,7 @@ def new_comment(
333
333
  {
334
334
  "comment_id": comment["id"],
335
335
  "task_id": task_id,
336
- "task_status_id": task_status_id
336
+ "task_status_id": task_status_id,
337
337
  },
338
338
  project_id=task["project_id"],
339
339
  )
@@ -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
@@ -1460,9 +1460,7 @@ def get_last_notifications(
1460
1460
  )
1461
1461
  else:
1462
1462
  reply_mentions = []
1463
- reply_department_mentions = (
1464
- []
1465
- )
1463
+ reply_department_mentions = []
1466
1464
 
1467
1465
  if role == "client" and is_current_user_artist:
1468
1466
  comment_text = ""
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
  )
@@ -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