zou 0.20.44__py3-none-any.whl → 0.20.46__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.44"
1
+ __version__ = "0.20.46"
zou/app/api.py CHANGED
@@ -1,9 +1,4 @@
1
- import os
2
1
  import sys
3
- import importlib
4
- import traceback
5
-
6
- from pathlib import Path
7
2
 
8
3
  from zou.app.blueprints.assets import blueprint as assets_blueprint
9
4
  from zou.app.blueprints.auth import blueprint as auth_blueprint
@@ -29,7 +24,7 @@ from zou.app.blueprints.user import blueprint as user_blueprint
29
24
  from zou.app.blueprints.edits import blueprint as edits_blueprint
30
25
  from zou.app.blueprints.concepts import blueprint as concepts_blueprint
31
26
 
32
- from zou.app.utils.plugins import PluginManifest
27
+ from zou.app.utils.plugins import load_plugins
33
28
  from zou.app.utils import events
34
29
 
35
30
 
@@ -92,40 +87,3 @@ def register_event_handlers(app):
92
87
  # app.logger.info("No event handlers folder is configured.")
93
88
  pass
94
89
  return app
95
-
96
-
97
- def load_plugins(app):
98
- """
99
- Load plugins from the plugin folder.
100
- """
101
- plugin_folder = app.config["PLUGIN_FOLDER"]
102
- abs_plugin_path = os.path.abspath(plugin_folder)
103
- if abs_plugin_path not in sys.path:
104
- sys.path.insert(0, abs_plugin_path)
105
-
106
- if os.path.exists(plugin_folder):
107
- for plugin_id in os.listdir(plugin_folder):
108
- try:
109
- load_plugin(app, plugin_id)
110
- app.logger.info(f"Plugin {plugin_id} loaded.")
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())
118
-
119
- if abs_plugin_path in sys.path:
120
- sys.path.remove(abs_plugin_path)
121
-
122
-
123
- def load_plugin(app, plugin_id):
124
- plugin_path = Path(app.config["PLUGIN_FOLDER"]) / plugin_id
125
- manifest = PluginManifest.from_file(plugin_path / "manifest.toml")
126
-
127
- plugin_module = importlib.import_module(plugin_id)
128
- if hasattr(plugin_module, "init_plugin"):
129
- plugin_module.init_plugin(app, manifest)
130
-
131
- return plugin_module
@@ -101,6 +101,13 @@ class PersonsResource(BaseModelsResource):
101
101
  raise
102
102
  except:
103
103
  raise WrongParameterException("Expiration date is not valid.")
104
+
105
+ if "email" in data:
106
+ try:
107
+ data["email"] = auth.validate_email(data["email"])
108
+ except auth.EmailNotValidException as e:
109
+ raise WrongParameterException(str(e))
110
+
104
111
  return data
105
112
 
106
113
  def update_data(self, data):
@@ -184,6 +191,13 @@ class PersonResource(BaseModelResource, ArgsMixin):
184
191
  raise
185
192
  except:
186
193
  raise WrongParameterException("Expiration date is not valid.")
194
+
195
+ if "email" in data:
196
+ try:
197
+ data["email"] = auth.validate_email(data["email"])
198
+ except auth.EmailNotValidException as e:
199
+ raise WrongParameterException(str(e))
200
+
187
201
  return data
188
202
 
189
203
  def check_delete_permissions(self, instance_dict):
@@ -30,6 +30,7 @@ from zou.app.blueprints.projects.resources import (
30
30
  ProductionBudgetResource,
31
31
  ProductionBudgetEntriesResource,
32
32
  ProductionBudgetEntryResource,
33
+ ProductionMonthTimeSpentsResource,
33
34
  )
34
35
 
35
36
  routes = [
@@ -127,6 +128,10 @@ routes = [
127
128
  "/data/projects/<project_id>/budgets/<budget_id>/entries/<entry_id>",
128
129
  ProductionBudgetEntryResource,
129
130
  ),
131
+ (
132
+ "/data/projects/<project_id>/budgets/time-spents",
133
+ ProductionMonthTimeSpentsResource,
134
+ ),
130
135
  ]
131
136
 
132
137
  blueprint = Blueprint("projects", "projects")
@@ -5,9 +5,11 @@ from flask_jwt_extended import jwt_required
5
5
  from zou.app.services import budget_service
6
6
  from zou.app.mixin import ArgsMixin
7
7
  from zou.app.services import (
8
+ persons_service,
8
9
  projects_service,
9
10
  schedule_service,
10
11
  tasks_service,
12
+ time_spents_service,
11
13
  user_service,
12
14
  )
13
15
  from zou.app.utils import permissions
@@ -1494,3 +1496,33 @@ class ProductionBudgetEntryResource(Resource, ArgsMixin):
1494
1496
  user_service.check_manager_project_access(project_id)
1495
1497
  budget_service.delete_budget_entry(entry_id)
1496
1498
  return "", 204
1499
+
1500
+
1501
+ class ProductionMonthTimeSpentsResource(Resource, ArgsMixin):
1502
+
1503
+ @jwt_required()
1504
+ def get(self, project_id):
1505
+ """
1506
+ Get aggregated time spents by month for given project.
1507
+ ---
1508
+ tags:
1509
+ - Projects
1510
+ parameters:
1511
+ - in: path
1512
+ name: project_id
1513
+ required: True
1514
+ type: string
1515
+ format: UUID
1516
+ x-example: a24a6ea4-ce75-4665-a070-57453082c25
1517
+ responses:
1518
+ 200:
1519
+ description: Aggregated time spents for given person and month
1520
+ 400:
1521
+ description: Wrong ID format
1522
+ """
1523
+ permissions.check_admin_permissions()
1524
+ self.check_id_parameter(project_id)
1525
+ user = persons_service.get_current_user()
1526
+ return time_spents_service.get_project_month_time_spents(
1527
+ project_id, user["timezone"]
1528
+ )
zou/app/config.py CHANGED
@@ -88,6 +88,7 @@ MAIL_USE_SSL = envtobool("MAIL_USE_SSL", False)
88
88
  MAIL_DEFAULT_SENDER = os.getenv(
89
89
  "MAIL_DEFAULT_SENDER", "no-reply@your-studio.com"
90
90
  )
91
+ MAIL_CHECK_DELIVERABILITY = envtobool("MAIL_CHECK_DELIVERABILITY", True)
91
92
  DOMAIN_NAME = os.getenv("DOMAIN_NAME", "localhost:8080")
92
93
  DOMAIN_PROTOCOL = os.getenv("DOMAIN_PROTOCOL", "https")
93
94
 
zou/app/models/plugin.py CHANGED
@@ -19,3 +19,4 @@ class Plugin(db.Model, BaseMixin, SerializerMixin):
19
19
  maintainer_email = db.Column(EmailType)
20
20
  website = db.Column(URLType)
21
21
  license = db.Column(db.String(80), nullable=False)
22
+ revision = db.Column(db.String(12), nullable=True)
@@ -1,11 +1,15 @@
1
- import zipfile
2
1
  import semver
3
- import shutil
4
2
  from pathlib import Path
5
3
 
6
4
  from zou.app import config, db
7
5
  from zou.app.models.plugin import Plugin
8
- from zou.app.utils.plugins import PluginManifest
6
+ from zou.app.utils.plugins import (
7
+ PluginManifest,
8
+ run_plugin_migrations,
9
+ downgrade_plugin_migrations,
10
+ uninstall_plugin_files,
11
+ install_plugin_files,
12
+ )
9
13
 
10
14
 
11
15
  def install_plugin(path, force=False):
@@ -20,7 +24,6 @@ def install_plugin(path, force=False):
20
24
  plugin = Plugin.query.filter_by(plugin_id=manifest.id).one_or_none()
21
25
 
22
26
  try:
23
- already_installed = False
24
27
  if plugin:
25
28
  current = semver.Version.parse(plugin.version)
26
29
  new = semver.Version.parse(str(manifest.version))
@@ -29,11 +32,13 @@ def install_plugin(path, force=False):
29
32
  f"Plugin version {new} is not newer than {current}."
30
33
  )
31
34
  plugin.update_no_commit(manifest.to_model_dict())
32
- already_installed = True
33
35
  else:
34
36
  plugin = Plugin.create_no_commit(**manifest.to_model_dict())
35
37
 
36
- install_plugin_files(manifest.id, path, already_installed)
38
+ plugin_path = install_plugin_files(
39
+ path, Path(config.PLUGIN_FOLDER) / manifest.id
40
+ )
41
+ run_plugin_migrations(plugin_path, plugin)
37
42
  except Exception:
38
43
  uninstall_plugin_files(manifest.id)
39
44
  db.session.rollback()
@@ -44,45 +49,13 @@ def install_plugin(path, force=False):
44
49
  return plugin.serialize()
45
50
 
46
51
 
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
52
  def uninstall_plugin(plugin_id):
82
53
  """
83
54
  Uninstall a plugin.
84
55
  """
85
- installed = uninstall_plugin_files(plugin_id)
56
+ plugin_path = Path(config.PLUGIN_FOLDER) / plugin_id
57
+ downgrade_plugin_migrations(plugin_path)
58
+ installed = uninstall_plugin_files(plugin_path)
86
59
  plugin = Plugin.query.filter_by(plugin_id=plugin_id).one_or_none()
87
60
  if plugin is not None:
88
61
  installed = True
@@ -91,79 +64,3 @@ def uninstall_plugin(plugin_id):
91
64
  if not installed:
92
65
  raise ValueError(f"Plugin '{plugin_id}' is not installed.")
93
66
  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
@@ -7,6 +7,7 @@ from sqlalchemy.exc import DataError
7
7
  from sqlalchemy.orm import aliased
8
8
 
9
9
  from zou.app.models.day_off import DayOff
10
+ from zou.app.models.department import Department
10
11
  from zou.app.models.project import Project
11
12
  from zou.app.models.task import Task
12
13
  from zou.app.models.task_type import TaskType
@@ -535,3 +536,38 @@ def get_timezoned_interval(start, end):
535
536
  """
536
537
  timezone = user_service.get_timezone()
537
538
  return date_helpers.get_timezoned_interval(start, end, timezone)
539
+
540
+
541
+ def get_project_month_time_spents(project_id, timezone=None):
542
+ """
543
+ Get aggregated time spents by department by person by month for given
544
+ project.
545
+ """
546
+ data = {}
547
+ query = (
548
+ TimeSpent.query
549
+ .join(Task)
550
+ .join(TaskType, TaskType.id == Task.task_type_id)
551
+ .join(Department, Department.id == TaskType.department_id)
552
+ .filter(Task.project_id == project_id)
553
+ .add_columns(Department.id)
554
+ .order_by(TimeSpent.date)
555
+ )
556
+
557
+ for time_spent, department_id in query.all():
558
+ date_key = date_helpers.get_simple_string_with_timezone_from_date(
559
+ time_spent.date, timezone
560
+ )[0:7]
561
+ if department_id not in data:
562
+ data[department_id] = { "total": 0 }
563
+ if time_spent.person_id not in data[department_id]:
564
+ data[department_id][time_spent.person_id] = { "total": 0 }
565
+ if date_key not in data[department_id][time_spent.person_id]:
566
+ data[department_id][time_spent.person_id][date_key] = 0
567
+
568
+ data[department_id][time_spent.person_id][date_key] += \
569
+ time_spent.duration
570
+ data[department_id]["total"] += time_spent.duration
571
+ data[department_id][time_spent.person_id]["total"] += \
572
+ time_spent.duration
573
+ return data
zou/app/utils/auth.py CHANGED
@@ -23,9 +23,13 @@ def encrypt_password(password):
23
23
  return flask_bcrypt.generate_password_hash(password)
24
24
 
25
25
 
26
- def validate_email(email):
26
+ def validate_email(
27
+ email, check_deliverability=config.MAIL_CHECK_DELIVERABILITY
28
+ ):
27
29
  try:
28
- return email_validator.validate_email(email).normalized
30
+ return email_validator.validate_email(
31
+ email, check_deliverability=check_deliverability
32
+ ).normalized
29
33
  except email_validator.EmailNotValidError as e:
30
34
  raise EmailNotValidException(str(e))
31
35
 
zou/app/utils/commands.py CHANGED
@@ -865,6 +865,9 @@ def list_plugins(output_format, verbose, filter_field, filter_value):
865
865
  if verbose:
866
866
  plugin_data["Description"] = plugin.description or "-"
867
867
  plugin_data["Website"] = plugin.website or "-"
868
+ plugin_data["Revision"] = plugin.revision or "-"
869
+ plugin_data["Installation Date"] = plugin.created_at
870
+ plugin_data["Last Update"] = plugin.updated_at
868
871
  plugin_list.append(plugin_data)
869
872
 
870
873
  if output_format == "table":
zou/app/utils/plugins.py CHANGED
@@ -3,6 +3,18 @@ import semver
3
3
  import email.utils
4
4
  import spdx_license_list
5
5
  import zipfile
6
+ import importlib
7
+ import importlib.util
8
+ import sys
9
+ import os
10
+ import traceback
11
+ import shutil
12
+
13
+ from pathlib import Path
14
+ from flask import current_app
15
+ from alembic import command
16
+ from alembic.config import Config
17
+
6
18
 
7
19
  from pathlib import Path
8
20
  from collections.abc import MutableMapping
@@ -86,3 +98,243 @@ class PluginManifest(MutableMapping):
86
98
  super().__setattr__(attr, value)
87
99
  else:
88
100
  self.data[attr] = value
101
+
102
+
103
+ def load_plugin(app, plugin_path, init_plugin=True):
104
+ """
105
+ Load a plugin from the plugin folder.
106
+ """
107
+ plugin_path = Path(plugin_path)
108
+ manifest = PluginManifest.from_plugin_path(plugin_path)
109
+
110
+ plugin_module = importlib.import_module(manifest["id"])
111
+ if init_plugin and hasattr(plugin_module, "init_plugin"):
112
+ plugin_module.init_plugin(app, manifest)
113
+
114
+ return plugin_module
115
+
116
+
117
+ def load_plugins(app):
118
+ """
119
+ Load plugins from the plugin folder.
120
+ """
121
+ plugin_folder = Path(app.config["PLUGIN_FOLDER"])
122
+ if plugin_folder.exists():
123
+ abs_plugin_path = str(plugin_folder.absolute())
124
+ if abs_plugin_path not in sys.path:
125
+ sys.path.insert(0, abs_plugin_path)
126
+
127
+ for plugin_id in os.listdir(plugin_folder):
128
+ try:
129
+ load_plugin(app, plugin_folder / plugin_id)
130
+ app.logger.info(f"Plugin {plugin_id} loaded.")
131
+ except ImportError as e:
132
+ app.logger.error(f"Plugin {plugin_id} failed to import: {e}")
133
+ except Exception as e:
134
+ app.logger.error(
135
+ f"Plugin {plugin_id} failed to initialize: {e}"
136
+ )
137
+ app.logger.debug(traceback.format_exc())
138
+
139
+ if abs_plugin_path in sys.path:
140
+ sys.path.remove(abs_plugin_path)
141
+
142
+
143
+ def migrate_plugin_db(plugin_path, message):
144
+ """
145
+ Generates Alembic migration files in path/migrations.
146
+ """
147
+ plugin_path = Path(plugin_path).absolute()
148
+ models_path = plugin_path / "models.py"
149
+
150
+ if not models_path.exists():
151
+ raise FileNotFoundError(f"'models.py' not found in '{plugin_path}'")
152
+
153
+ manifest = PluginManifest.from_plugin_path(plugin_path)
154
+
155
+ module_name = f"_plugin_models_{manifest['id']}"
156
+ spec = importlib.util.spec_from_file_location(module_name, models_path)
157
+ if spec is None or spec.loader is None:
158
+ raise ImportError(f"Could not load 'models.py' from '{plugin_path}'")
159
+
160
+ module = importlib.util.module_from_spec(spec)
161
+ sys.modules[module_name] = module
162
+ try:
163
+ spec.loader.exec_module(module)
164
+ migrations_dir = plugin_path / "migrations"
165
+ versions_dir = migrations_dir / "versions"
166
+ versions_dir.mkdir(parents=True, exist_ok=True)
167
+
168
+ alembic_cfg = Config()
169
+ alembic_cfg.config_file_name = str(
170
+ plugin_path / "migrations" / "alembic.ini"
171
+ )
172
+ alembic_cfg.set_main_option("script_location", str(migrations_dir))
173
+ alembic_cfg.set_main_option(
174
+ "sqlalchemy.url", current_app.config["SQLALCHEMY_DATABASE_URI"]
175
+ )
176
+
177
+ command.revision(alembic_cfg, autogenerate=True, message=message)
178
+ finally:
179
+ del sys.modules[module_name]
180
+
181
+
182
+ def run_plugin_migrations(plugin_path, plugin):
183
+ """
184
+ Run plugin migrations.
185
+ """
186
+ plugin_path = Path(plugin_path)
187
+
188
+ alembic_cfg = Config()
189
+ alembic_cfg.config_file_name = str(
190
+ plugin_path / "migrations" / "alembic.ini"
191
+ )
192
+ alembic_cfg.set_main_option(
193
+ "script_location", str(plugin_path / "migrations")
194
+ )
195
+ alembic_cfg.set_main_option(
196
+ "sqlalchemy.url", current_app.config["SQLALCHEMY_DATABASE_URI"]
197
+ )
198
+
199
+ command.upgrade(alembic_cfg, "head")
200
+
201
+ script = command.ScriptDirectory.from_config(alembic_cfg)
202
+ head_revision = script.get_current_head()
203
+
204
+ plugin.revision = head_revision
205
+
206
+ return head_revision
207
+
208
+
209
+ def downgrade_plugin_migrations(plugin_path):
210
+ """
211
+ Downgrade plugin migrations to base.
212
+ """
213
+ plugin_path = Path(plugin_path)
214
+ manifest = PluginManifest.from_plugin_path(plugin_path)
215
+
216
+ alembic_cfg = Config()
217
+ alembic_cfg.config_file_name = str(
218
+ plugin_path / "migrations" / "alembic.ini"
219
+ )
220
+ alembic_cfg.set_main_option(
221
+ "script_location", str(plugin_path / "migrations")
222
+ )
223
+ alembic_cfg.set_main_option(
224
+ "sqlalchemy.url", current_app.config["SQLALCHEMY_DATABASE_URI"]
225
+ )
226
+
227
+ try:
228
+ command.downgrade(alembic_cfg, "base")
229
+ except Exception as e:
230
+ current_app.logger.warning(
231
+ f"Downgrade failed for plugin {manifest.id}: {e}"
232
+ )
233
+
234
+
235
+ def create_plugin_package(path, output_path, force=False):
236
+ """
237
+ Create a plugin package.
238
+ """
239
+ path = Path(path)
240
+ if not path.exists():
241
+ raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
242
+
243
+ manifest = PluginManifest.from_plugin_path(path)
244
+
245
+ output_path = Path(output_path)
246
+ if not output_path.suffix == ".zip":
247
+ output_path /= f"{manifest.id}-{manifest.version}.zip"
248
+ if output_path.exists():
249
+ if force:
250
+ output_path.unlink()
251
+ else:
252
+ raise FileExistsError(
253
+ f"Output path '{output_path}' already exists."
254
+ )
255
+
256
+ output_path = shutil.make_archive(
257
+ output_path.with_suffix(""),
258
+ "zip",
259
+ path,
260
+ )
261
+ return output_path
262
+
263
+
264
+ def create_plugin_skeleton(
265
+ path,
266
+ id,
267
+ name,
268
+ description=None,
269
+ version=None,
270
+ maintainer=None,
271
+ website=None,
272
+ license=None,
273
+ force=False,
274
+ ):
275
+ plugin_template_path = (
276
+ Path(__file__).parent.parent.parent / "plugin_template"
277
+ )
278
+ plugin_path = Path(path) / id
279
+
280
+ if plugin_path.exists():
281
+ if force:
282
+ shutil.rmtree(plugin_path)
283
+ else:
284
+ raise FileExistsError(
285
+ f"Plugin '{id}' already exists in {plugin_path}."
286
+ )
287
+
288
+ shutil.copytree(plugin_template_path, plugin_path)
289
+
290
+ manifest = PluginManifest.from_file(plugin_path / "manifest.toml")
291
+
292
+ manifest.id = id
293
+ manifest.name = name
294
+ if description:
295
+ manifest.description = description
296
+ if version:
297
+ manifest.version = version
298
+ if maintainer:
299
+ manifest.maintainer = maintainer
300
+ if website:
301
+ manifest.website = website
302
+ if license:
303
+ manifest.license = license
304
+
305
+ manifest.validate()
306
+ manifest.write_to_path(plugin_path)
307
+
308
+ return plugin_path
309
+
310
+
311
+ def install_plugin_files(files_path, installation_path):
312
+ """
313
+ Install plugin files.
314
+ """
315
+ files_path = Path(files_path)
316
+ installation_path = Path(installation_path)
317
+
318
+ installation_path.mkdir(parents=True, exist_ok=True)
319
+
320
+ if files_path.is_dir():
321
+ shutil.copytree(files_path, installation_path, dirs_exist_ok=True)
322
+ elif zipfile.is_zipfile(files_path):
323
+ shutil.unpack_archive(files_path, installation_path, format="zip")
324
+ else:
325
+ raise ValueError(
326
+ f"Plugin path '{files_path}' is not a valid zip file or a directory."
327
+ )
328
+
329
+ return installation_path
330
+
331
+
332
+ def uninstall_plugin_files(plugin_path):
333
+ """
334
+ Uninstall plugin files.
335
+ """
336
+ plugin_path = Path(plugin_path)
337
+ if plugin_path.exists():
338
+ shutil.rmtree(plugin_path)
339
+ return True
340
+ return False
zou/cli.py CHANGED
@@ -8,7 +8,7 @@ import traceback
8
8
 
9
9
  from sqlalchemy.exc import IntegrityError
10
10
 
11
- from zou.app.utils import dbhelpers, auth, commands
11
+ from zou.app.utils import dbhelpers, auth, commands, plugins as plugin_utils
12
12
  from zou.app.services import persons_service, auth_service, plugins_service
13
13
  from zou.app.services.exception import (
14
14
  IsUserLimitReachedException,
@@ -195,8 +195,8 @@ def create_admin(email, password):
195
195
  except auth.PasswordTooShortException:
196
196
  print("Password is too short.")
197
197
  sys.exit(1)
198
- except auth.EmailNotValidException:
199
- print("Email is not valid.")
198
+ except auth.EmailNotValidException as e:
199
+ print(f"Email is not valid: {e}")
200
200
  sys.exit(1)
201
201
  except IsUserLimitReachedException:
202
202
  print(f"User limit reached (limit {config.USER_LIMIT}).")
@@ -738,7 +738,7 @@ def create_plugin_skeleton(
738
738
  """
739
739
  Create a plugin skeleton.
740
740
  """
741
- plugin_path = plugins_service.create_plugin_skeleton(
741
+ plugin_path = plugin_utils.create_plugin_skeleton(
742
742
  path,
743
743
  id,
744
744
  name,
@@ -775,9 +775,7 @@ def create_plugin_package(
775
775
  """
776
776
  Create a plugin package.
777
777
  """
778
- plugin_path = plugins_service.create_plugin_package(
779
- path, output_path, force
780
- )
778
+ plugin_path = plugin_utils.create_plugin_package(path, output_path, force)
781
779
  print(f"Plugin package created in '{plugin_path}'.")
782
780
 
783
781
 
@@ -811,5 +809,20 @@ def list_plugins(output_format, verbose, filter_field, filter_value):
811
809
  commands.list_plugins(output_format, verbose, filter_field, filter_value)
812
810
 
813
811
 
812
+ @cli.command()
813
+ @click.option(
814
+ "--path",
815
+ required=True,
816
+ )
817
+ @click.option("--message", default="")
818
+ def migrate_plugin_db(path, message):
819
+ """
820
+ Migrate plugin database.
821
+ """
822
+ with app.app_context():
823
+ plugin_utils.migrate_plugin_db(path, message)
824
+ print(f"Plugin {path} database migrated.")
825
+
826
+
814
827
  if __name__ == "__main__":
815
828
  cli()
zou/migrations/env.py CHANGED
@@ -1,90 +1,26 @@
1
- from __future__ import with_statement
2
-
1
+ import logging
3
2
 
4
3
  from alembic import context
5
4
  from sqlalchemy import create_engine, pool
6
5
  from logging.config import fileConfig
7
6
  from flask import current_app
8
7
 
9
- import logging
8
+ from zou.app import db
10
9
 
11
- # this is the Alembic Config object, which provides
12
- # access to the values within the .ini file in use.
10
+ # Database URL (passed by Alembic)
13
11
  config = context.config
12
+ url = current_app.config.get("SQLALCHEMY_DATABASE_URI")
14
13
 
15
14
  # Interpret the config file for Python logging.
16
15
  # This line sets up loggers basically.
17
16
  fileConfig(config.config_file_name)
18
17
  logger = logging.getLogger("alembic.env")
19
18
 
20
- # add your model's MetaData object here
21
- # for 'autogenerate' support
22
- # from myapp import mymodel
23
- # target_metadata = mymodel.Base.metadata
24
- target_metadata = current_app.extensions["migrate"].db.metadata
25
-
26
- # other values from the config, defined by the needs of env.py,
27
- # can be acquired:
28
- # my_important_option = config.get_main_option("my_important_option")
29
- # ... etc.
30
-
31
-
32
- def run_migrations_offline():
33
- """Run migrations in 'offline' mode.
34
19
 
35
- This configures the context with just a URL
36
- and not an Engine, though an Engine is acceptable
37
- here as well. By skipping the Engine creation
38
- we don't even need a DBAPI to be available.
39
-
40
- Calls to context.execute() here emit the given string to the
41
- script output.
42
-
43
- """
44
- url = current_app.config.get("SQLALCHEMY_DATABASE_URI")
45
- context.configure(url=url, compare_type=True)
46
-
47
- with context.begin_transaction():
48
- context.run_migrations()
49
-
50
-
51
- def run_migrations_online():
52
- """Run migrations in 'online' mode.
53
-
54
- In this scenario we need to create an Engine
55
- and associate a connection with the context.
56
-
57
- """
58
-
59
- # this callback is used to prevent an auto-migration from being generated
60
- # when there are no changes to the schema
61
- # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
62
- def process_revision_directives(context, revision, directives):
63
- if getattr(config.cmd_opts, "autogenerate", False):
64
- script = directives[0]
65
- if script.upgrade_ops.is_empty():
66
- directives[:] = []
67
- logger.info("No changes in schema detected.")
68
-
69
- engine = create_engine(
70
- current_app.config.get("SQLALCHEMY_DATABASE_URI"),
71
- poolclass=pool.NullPool,
72
- )
73
-
74
- connection = engine.connect()
75
- context.configure(
76
- connection=connection,
77
- target_metadata=target_metadata,
78
- process_revision_directives=process_revision_directives,
79
- render_item=render_item,
80
- **current_app.extensions["migrate"].configure_args
81
- )
82
-
83
- try:
84
- with context.begin_transaction():
85
- context.run_migrations()
86
- finally:
87
- connection.close()
20
+ def include_object(object, name, type_, reflected, compare_to):
21
+ if type_ == "table":
22
+ return not reflected or name in db.metadata.tables
23
+ return True
88
24
 
89
25
 
90
26
  def render_item(type_, obj, autogen_context):
@@ -102,7 +38,27 @@ def render_item(type_, obj, autogen_context):
102
38
  return False
103
39
 
104
40
 
105
- if context.is_offline_mode():
106
- run_migrations_offline()
107
- else:
108
- run_migrations_online()
41
+ def process_revision_directives(context, revision, directives):
42
+ if getattr(config.cmd_opts, "autogenerate", False):
43
+ script = directives[0]
44
+ if script.upgrade_ops.is_empty():
45
+ directives[:] = []
46
+ logger.info("No changes in schema detected.")
47
+
48
+
49
+ def run_migrations_online():
50
+ connectable = create_engine(url, poolclass=pool.NullPool)
51
+ with connectable.connect() as connection:
52
+ context.configure(
53
+ connection=connection,
54
+ target_metadata=db.metadata,
55
+ process_revision_directives=process_revision_directives,
56
+ render_item=render_item,
57
+ include_object=include_object,
58
+ **current_app.extensions["migrate"].configure_args,
59
+ )
60
+ with context.begin_transaction():
61
+ context.run_migrations()
62
+
63
+
64
+ run_migrations_online()
@@ -0,0 +1,67 @@
1
+ """Add plugin.revision
2
+
3
+ Revision ID: d80f02824047
4
+ Revises: e7e633bd6fa2
5
+ Create Date: 2025-05-02 16:08:57.078114
6
+
7
+ """
8
+
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "d80f02824047"
15
+ down_revision = "e7e633bd6fa2"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade():
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ with op.batch_alter_table("plugin", schema=None) as batch_op:
23
+ batch_op.add_column(
24
+ sa.Column("revision", sa.String(length=12), nullable=True)
25
+ )
26
+
27
+ with op.batch_alter_table("salary_scale", schema=None) as batch_op:
28
+ batch_op.alter_column(
29
+ "position", existing_type=sa.VARCHAR(length=255), nullable=True
30
+ )
31
+ batch_op.alter_column(
32
+ "seniority", existing_type=sa.VARCHAR(length=255), nullable=True
33
+ )
34
+
35
+ with op.batch_alter_table("task", schema=None) as batch_op:
36
+ batch_op.alter_column(
37
+ "difficulty",
38
+ existing_type=sa.INTEGER(),
39
+ nullable=False,
40
+ existing_server_default=sa.text("3"),
41
+ )
42
+
43
+ # ### end Alembic commands ###
44
+
45
+
46
+ def downgrade():
47
+ # ### commands auto generated by Alembic - please adjust! ###
48
+ with op.batch_alter_table("task", schema=None) as batch_op:
49
+ batch_op.alter_column(
50
+ "difficulty",
51
+ existing_type=sa.INTEGER(),
52
+ nullable=True,
53
+ existing_server_default=sa.text("3"),
54
+ )
55
+
56
+ with op.batch_alter_table("salary_scale", schema=None) as batch_op:
57
+ batch_op.alter_column(
58
+ "seniority", existing_type=sa.VARCHAR(length=255), nullable=False
59
+ )
60
+ batch_op.alter_column(
61
+ "position", existing_type=sa.VARCHAR(length=255), nullable=False
62
+ )
63
+
64
+ with op.batch_alter_table("plugin", schema=None) as batch_op:
65
+ batch_op.drop_column("revision")
66
+
67
+ # ### end Alembic commands ###
@@ -0,0 +1,45 @@
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # template used to generate migration files
5
+ # file_template = %%(rev)s_%%(slug)s
6
+
7
+ # set to 'true' to run the environment during
8
+ # the 'revision' command, regardless of autogenerate
9
+ # revision_environment = false
10
+
11
+
12
+ # Logging configuration
13
+ [loggers]
14
+ keys = root,sqlalchemy,alembic
15
+
16
+ [handlers]
17
+ keys = console
18
+
19
+ [formatters]
20
+ keys = generic
21
+
22
+ [logger_root]
23
+ level = WARN
24
+ handlers = console
25
+ qualname =
26
+
27
+ [logger_sqlalchemy]
28
+ level = WARN
29
+ handlers =
30
+ qualname = sqlalchemy.engine
31
+
32
+ [logger_alembic]
33
+ level = INFO
34
+ handlers =
35
+ qualname = alembic
36
+
37
+ [handler_console]
38
+ class = StreamHandler
39
+ args = (sys.stderr,)
40
+ level = NOTSET
41
+ formatter = generic
42
+
43
+ [formatter_generic]
44
+ format = %(levelname)-5.5s [%(name)s] %(message)s
45
+ datefmt = %H:%M:%S
@@ -0,0 +1,67 @@
1
+ import importlib.util
2
+ import logging
3
+ import sys
4
+ from alembic import context
5
+ from sqlalchemy import create_engine, pool
6
+ from pathlib import Path
7
+ from logging.config import fileConfig
8
+
9
+ from zou.app.utils.plugins import PluginManifest
10
+
11
+ plugin_path = Path(__file__).resolve().parents[1]
12
+ models_path = plugin_path / "models.py"
13
+ manifest = PluginManifest.from_plugin_path(plugin_path)
14
+
15
+ module_name = f"_plugin_models_{manifest['id']}"
16
+ spec = importlib.util.spec_from_file_location(module_name, models_path)
17
+ module = importlib.util.module_from_spec(spec)
18
+ sys.modules[module_name] = module
19
+ spec.loader.exec_module(module)
20
+
21
+ # Database URL (passed by Alembic)
22
+ config = context.config
23
+ url = config.get_main_option("sqlalchemy.url")
24
+
25
+ # Interpret the config file for Python logging.
26
+ # This line sets up loggers basically.
27
+ fileConfig(config.config_file_name)
28
+ logger = logging.getLogger("alembic.env")
29
+
30
+
31
+ def include_object(object, name, type_, reflected, compare_to):
32
+ if type_ == "table":
33
+ return not reflected or name in module.plugin_metadata.tables
34
+ return True
35
+
36
+
37
+ def render_item(type_, obj, autogen_context):
38
+ """Apply custom rendering for selected items."""
39
+
40
+ import sqlalchemy_utils
41
+
42
+ if type_ == "type" and isinstance(
43
+ obj, sqlalchemy_utils.types.uuid.UUIDType
44
+ ):
45
+ autogen_context.imports.add("import sqlalchemy_utils")
46
+ autogen_context.imports.add("import uuid")
47
+ return "sqlalchemy_utils.types.uuid.UUIDType(binary=False), default=uuid.uuid4"
48
+
49
+ return False
50
+
51
+
52
+ def run_migrations_online():
53
+ connectable = create_engine(url, poolclass=pool.NullPool)
54
+ with connectable.connect() as connection:
55
+ context.configure(
56
+ connection=connection,
57
+ target_metadata=module.plugin_metadata,
58
+ version_table=f"alembic_version_{manifest['id']}",
59
+ compare_type=True,
60
+ include_object=include_object,
61
+ render_item=render_item,
62
+ )
63
+ with context.begin_transaction():
64
+ context.run_migrations()
65
+
66
+
67
+ run_migrations_online()
@@ -0,0 +1,25 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ import sqlalchemy_utils
11
+ ${imports if imports else ""}
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = ${repr(up_revision)}
15
+ down_revision = ${repr(down_revision)}
16
+ branch_labels = ${repr(branch_labels)}
17
+ depends_on = ${repr(depends_on)}
18
+
19
+
20
+ def upgrade():
21
+ ${upgrades if upgrades else "pass"}
22
+
23
+
24
+ def downgrade():
25
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,17 @@
1
+ from zou.app.models.serializer import SerializerMixin
2
+ from zou.app.models.base import BaseMixin
3
+
4
+ from sqlalchemy.orm import declarative_base
5
+ from sqlalchemy import MetaData, Column, Integer
6
+
7
+ plugin_metadata = MetaData()
8
+ PluginBase = declarative_base(metadata=plugin_metadata)
9
+
10
+
11
+ class Count(PluginBase, BaseMixin, SerializerMixin):
12
+ """
13
+ Describe a plugin.
14
+ """
15
+
16
+ __tablename__ = "toto_count"
17
+ count = Column(Integer, nullable=False, default=0)
@@ -1,6 +1,13 @@
1
1
  from flask_restful import Resource
2
+ from .models import Count
2
3
 
3
4
 
4
5
  class HelloWorld(Resource):
5
6
  def get(self):
6
- return {"message": "Hello, World!"}
7
+ if not Count.query.first():
8
+ c = Count.create()
9
+ else:
10
+ c = Count.query.first()
11
+ c.count += 1
12
+ Count.commit()
13
+ return {"message": "Hello, World!", "count": c.count}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zou
3
- Version: 0.20.44
3
+ Version: 0.20.46
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
@@ -1,11 +1,11 @@
1
- zou/__init__.py,sha256=KEP4Hp-ErPBasQszaDcDgYAc7GdRcpNqDQOnsCD6EUw,24
2
- zou/cli.py,sha256=Mes4MNjmPFxrAMHkiJtmU4G3F9VMvFJfieOAyt2Irb8,22064
1
+ zou/__init__.py,sha256=nTukW1EMVbcsuCfvx3b13auDCAZFwK7mzCLf3SiMFJc,24
2
+ zou/cli.py,sha256=8YdBqJKEulAZ_1ohTdmpt_lf8EMjDmYiH8l0jVNlfG0,22397
3
3
  zou/debug.py,sha256=1fawPbkD4wn0Y9Gk0BiBFSa-CQe5agFi8R9uJYl2Uyk,520
4
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=TYWUC__i23Lbpd2ahfJ_0Ny0fGieAluT5EP_wd9cQKw,5033
8
- zou/app/config.py,sha256=eXVrmZf550Tk0fiN0Asfrhel0StesTDLTAYU6LdM3n4,6747
7
+ zou/app/api.py,sha256=Esgni_0-tAaE3msWxNMEdUS_wwIdElM0P2GrjhaHbCk,3727
8
+ zou/app/config.py,sha256=6Yka0ox6GFOOYZavSDxAiFAslgFMJs12wpypPDjxjXs,6820
9
9
  zou/app/mixin.py,sha256=MGRrwLLRjWQtXHZ1YTaMgR5Jc8khnOrFqkvy2hzP5QY,5211
10
10
  zou/app/swagger.py,sha256=Jr7zsMqJi0V4FledODOdu-aqqVE02jMFzhqVxHK0_2c,54158
11
11
  zou/app/blueprints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -45,7 +45,7 @@ zou/app/blueprints/crud/notification.py,sha256=A-KNH0IDNCXlE4AddNxvmsD_7a9HqHGo_
45
45
  zou/app/blueprints/crud/organisation.py,sha256=P1yzIwVB1AE0-ZfiZAVZkUl7hshIwV8OL0NF3DZS7gQ,1340
46
46
  zou/app/blueprints/crud/output_file.py,sha256=tz0NOoEUobNa8RMjl195S6uNP7PsPM-ajMOYWdHUhhE,3968
47
47
  zou/app/blueprints/crud/output_type.py,sha256=Uu7Q5iv-4SZDnxHkLk0vUE1N4k0bBUivXQUTsc58BU8,2012
48
- zou/app/blueprints/crud/person.py,sha256=k1EcYxD5C5p7tquAvJUpD2873-ITeF2ogMNIYRxWuDo,9427
48
+ zou/app/blueprints/crud/person.py,sha256=Ot1lTdMRm7BWaOuuTGGm-vrT9SlTlLueOJKR8aEBogY,9869
49
49
  zou/app/blueprints/crud/playlist.py,sha256=he8iXoWnjBVXzkB_y8aGnZ6vQ_7hGSf-ALofLFoqx1U,1890
50
50
  zou/app/blueprints/crud/plugin.py,sha256=azDWurAX8KUD4FXwhCpo8q0qRUVG8TG4Dg8oib1aOzc,356
51
51
  zou/app/blueprints/crud/preview_background_file.py,sha256=TRJlVQ3nGOYVkA6kxIlNrip9bjkUaknVNCIUYw8uJYk,2451
@@ -96,8 +96,8 @@ zou/app/blueprints/playlists/__init__.py,sha256=vuEk1F3hFHsmuKWhdepMoLyOzmNKDn1Y
96
96
  zou/app/blueprints/playlists/resources.py,sha256=mF3gmlWpe9YKyyKVRUXYtvTCk7OguWIMVaCN_J_JPS4,17636
97
97
  zou/app/blueprints/previews/__init__.py,sha256=ihC6OQ9AUjnZ2JeMnjRh_tKGO0UmAjOwhZnOivc3BnQ,4460
98
98
  zou/app/blueprints/previews/resources.py,sha256=uyjfW3vyE2a1PPXO8MsHP8-3jhuVKHt3oi2pYsq-ZIw,53376
99
- zou/app/blueprints/projects/__init__.py,sha256=setMMX-aPrAUUuRJCdH2kJaYMwSMB6XbT9Tx6c63dBY,4489
100
- zou/app/blueprints/projects/resources.py,sha256=fPtUb16ptifM1iplMhaDajB6RhRqp0cJQvnEZPwlEx4,44024
99
+ zou/app/blueprints/projects/__init__.py,sha256=KhzIn5HvemfvsP5yJBMImdsEuTA_P806kRpoNbAdfIs,4643
100
+ zou/app/blueprints/projects/resources.py,sha256=7WAJU0Y7-IzDvsybsPk826yGEQZA84nbGy0xfn-_d8g,44955
101
101
  zou/app/blueprints/search/__init__.py,sha256=QCjQIY_85l_orhdEiqav_GifjReuwsjZggN3V0GeUVY,356
102
102
  zou/app/blueprints/search/resources.py,sha256=_QgRlUuxCPgY-ip5r2lGFtXNcGSE579JsCSrVf8ajVU,3093
103
103
  zou/app/blueprints/shots/__init__.py,sha256=EcG9qmAchlucqg1M6-RqWGfuKpa5Kq6RgyLZNSsjUr4,4225
@@ -167,7 +167,7 @@ zou/app/models/output_file.py,sha256=hyLGrpsgrk0aisDXppRQrB7ItCwyuyw-X0ZwVAHabsA
167
167
  zou/app/models/output_type.py,sha256=us_lCUCEvuP4vi_XmmOcEl1J2MtZhMX5ZheBqEFCgWA,381
168
168
  zou/app/models/person.py,sha256=xRjoPL2OayjmwUJcvbD7BJY7atxm-daIzPfEhgr5LbU,8107
169
169
  zou/app/models/playlist.py,sha256=YGgAk84u0_fdIEY02Dal4kfk8APVZvWFwWYV74qvrio,1503
170
- zou/app/models/plugin.py,sha256=2VUKrnoQaZDMG9vTmNsk51uuUUzhN6Xhh-E9xH85Js4,711
170
+ zou/app/models/plugin.py,sha256=zNs9Qi1h_v_6edZs5GHRSKItaAG4Gnlk1FQKumiiv68,766
171
171
  zou/app/models/preview_background_file.py,sha256=j8LgRmY7INnlB07hFwwB-8ssQrRC8vsb8VcpsTbt6tA,559
172
172
  zou/app/models/preview_file.py,sha256=eDPXw0QIdJze_E4kAS8SsyabrefWhIIdwuGmjss7iXo,3282
173
173
  zou/app/models/project.py,sha256=mXoLK7fEwDRGd21AoQadgV9K0AgoxYwTb5Sdf_cezlY,9203
@@ -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/plugins_service.py,sha256=SW0lTQCLJTT-Gxi8pEAmtyMOF97ebOyTHUBYqZ36c88,4422
214
+ zou/app/services/plugins_service.py,sha256=dbU-2f7t3eo6VISQBsASM8Or4Y8ha6Vt8ZHgtizdOi0,1917
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
@@ -222,7 +222,7 @@ zou/app/services/status_automations_service.py,sha256=tVio7Sj7inhvKS4UOyRhcdpwr_
222
222
  zou/app/services/sync_service.py,sha256=iWxx1kOGEXympHmSBBQWtDZWNtumdxp8kppee0OefMo,41811
223
223
  zou/app/services/tasks_service.py,sha256=D-u-8W3rIg00Nqp7MuG1WSOcPANE4XXliKpVwm5NbVU,69815
224
224
  zou/app/services/telemetry_services.py,sha256=xQm1h1t_JxSFW59zQGf4NuNdUi1UfMa_6pQ-ytRbmGA,1029
225
- zou/app/services/time_spents_service.py,sha256=H9X-60s6oqtY9rtU-K2jKwUSljfkdGlf_9wMr3iVfIA,15158
225
+ zou/app/services/time_spents_service.py,sha256=sjyDqQKDpnmDm-lPLCd2FxYBE6pOVZEgommwZcAjGk8,16498
226
226
  zou/app/services/user_service.py,sha256=rvo_e_JkPdTqIOE3FETXlUXnuS8n0lLyovTpwApyt8I,51470
227
227
  zou/app/stores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
228
228
  zou/app/stores/auth_tokens_store.py,sha256=-qOJPybLHvnMOq3PWk073OW9HJwOHGhFLZeOIlX1UVw,1290
@@ -231,11 +231,11 @@ zou/app/stores/publisher_store.py,sha256=ggpb68i_wCfcuWicmxv8B1YPLXwHmcZo2reqcLn
231
231
  zou/app/stores/queue_store.py,sha256=udbZSm3Rfwi-zwSRzVz-0qjdrVYbUWA8WwuIsdQikn8,669
232
232
  zou/app/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
233
233
  zou/app/utils/api.py,sha256=28ys2SMso1wt0q7r-4ohx9Izx-cKHxQhCmxfTvTMeFs,705
234
- zou/app/utils/auth.py,sha256=DZfZSr1Ulge0UK3hfvOWsMo3_d7RVP_llV118u9BtUI,870
234
+ zou/app/utils/auth.py,sha256=o9lOGgj3nkfsupfhLBre5fUuT1HBZgXVJU06lrdZGag,996
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=rd93hTuF-kjmIVBnpBL1uOPDfwY10wJIyu9DQKkbbNM,29785
238
+ zou/app/utils/commands.py,sha256=19ibYVCXbT9H8DdxT5bKRueGJV4Hab-WliSow9sL6dc,29982
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,7 +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
+ zou/app/utils/plugins.py,sha256=6mjt3nijyPBdWpkmMnsbefSooXv4lqvxWG-7AF4PeQ8,9622
254
254
  zou/app/utils/query.py,sha256=q8ETGPAqnz0Pt9xWoQt5o7FFAVYUKVCJiWpwefIr-iU,4592
255
255
  zou/app/utils/redis.py,sha256=xXEh9pl-3qPbr89dKHvcXSUTC6hd77vv_N8PVcRRZTE,377
256
256
  zou/app/utils/remote_job.py,sha256=QPxcCWEv-NM1Q4IQawAyJAiSORwkMeOlByQb9OCShEw,2522
@@ -261,7 +261,7 @@ zou/app/utils/thumbnail.py,sha256=eUb25So1fbjxZKYRpTOxLcZ0vww5zXNVkfVIa_tu5Dk,66
261
261
  zou/migrations/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
262
262
  zou/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
263
263
  zou/migrations/alembic.ini,sha256=zQU53x-FQXAbtuOxp3_hgtsEZK8M0Unkw9F_uMSBEDc,770
264
- zou/migrations/env.py,sha256=2VGkeMZzqLkGc7wMUowvIAO0OKe1KhoO9QlCRGiymWQ,3184
264
+ zou/migrations/env.py,sha256=lAFtfge8JEI70kC-I_Juv6hTYN4hcbprCrAIdooE9mI,1928
265
265
  zou/migrations/script.py.mako,sha256=FrHLRpJcpRUI3pK_qYEohmCNbc9JRJFNP0P8Atjj-iw,518
266
266
  zou/migrations/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
267
267
  zou/migrations/utils/base.py,sha256=vRzhgHxZXdpAqO3jfMl68lOnFukucb-n6sPxHGZ2xAQ,511
@@ -407,6 +407,7 @@ zou/migrations/versions/cf3d365de164_add_entity_version_model.py,sha256=n3k3ojRQ
407
407
  zou/migrations/versions/cf6cec6d6bf5_add_status_field_to_preview_file.py,sha256=0c8_OghAaaM0R6bKbs7MADvOkZ1sFE3SVp_nNB2ACgQ,950
408
408
  zou/migrations/versions/d25118cddcaa_modify_salary_scale_model.py,sha256=hjrfR0tnxAAfVgAG_Fqi2He4PqDyoqX6pOaj5TrmEJc,3814
409
409
  zou/migrations/versions/d80267806131_task_status_new_column_is_default.py,sha256=7HtK0bfBUh9MrJIbpUgz6S-Ye_R_4DbHILpODMBVVwE,2610
410
+ zou/migrations/versions/d80f02824047_add_plugin_revision.py,sha256=xaVdA7_a3MXXu1McNqpBstN_HF1oyp9i2RO4rYBbkSs,1944
410
411
  zou/migrations/versions/d8dcd5196d57_add_casting_label.py,sha256=TO4dlWblBzwAcs9Vx4fJVoMsdy2XuF7OncFn8N6BSe8,687
411
412
  zou/migrations/versions/de8a3de227ef_.py,sha256=3spU6b2kSApcMi9NomIRQGmoNtYos137krVrmdJWLCQ,1319
412
413
  zou/migrations/versions/deeacd38d373_for_projecttaskstatuslink_set_default_.py,sha256=eJsmrxTv1AP47_M0wXzUxIUf3xr904WY05MNYmnCcCg,1821
@@ -434,16 +435,20 @@ zou/migrations/versions/fee7c696166e_.py,sha256=jVvkr0uY5JPtC_I9h18DELo_eRu99RdO
434
435
  zou/migrations/versions/feffd3c5b806_introduce_concepts.py,sha256=wFNAtTTDHa01AdoXhRDayCVdB33stUFn6whc3opg2vQ,4774
435
436
  zou/migrations/versions/ffeed4956ab1_add_more_details_to_projects.py,sha256=cgRcg4IRW9kJ-A9-fVb5y5IXwRd74czomSVqMz3fVJo,1164
436
437
  zou/plugin_template/__init__.py,sha256=f-p3Ds5rRK48camU6WCflKkpdYi9IhLPaifqluFygVw,772
437
- zou/plugin_template/routes.py,sha256=cE4DAsLXBSPzigj2B_xWNdxYS9AG9uPhiDIgsE_okW4,128
438
+ zou/plugin_template/models.py,sha256=ExGehrSCC5vhrFeabgqicDZclLiFlIyUHonTOC61A_4,459
439
+ zou/plugin_template/routes.py,sha256=w0KNl2YoErxwnCoTt1EQiaUUCDDgSTNJd9dY6wfCCV4,341
440
+ zou/plugin_template/migrations/alembic.ini,sha256=zQU53x-FQXAbtuOxp3_hgtsEZK8M0Unkw9F_uMSBEDc,770
441
+ zou/plugin_template/migrations/env.py,sha256=QEv3_y4ynLhq2kiH6Huw0yUKW-KTjzoqn7TGB6Ryxbo,2053
442
+ zou/plugin_template/migrations/script.py.mako,sha256=FrHLRpJcpRUI3pK_qYEohmCNbc9JRJFNP0P8Atjj-iw,518
438
443
  zou/remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
439
444
  zou/remote/config_payload.py,sha256=pcKy6bJak41lc8Btq0gYMpHRJuG8y2Ae7lbZDOdchNQ,1907
440
445
  zou/remote/normalize_movie.py,sha256=zNfEY3N1UbAHZfddGONTg2Sff3ieLVWd4dfZa1dpnes,2164
441
446
  zou/remote/playlist.py,sha256=AsDo0bgYhDcd6DfNRV6r6Jj3URWwavE2ZN3VkKRPbLU,3293
442
447
  zou/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
443
448
  zou/utils/movie.py,sha256=d67fIL9dVBKt-E_qCGXRbNNdbJaJR5sHvZeX3hf8ldE,16559
444
- zou-0.20.44.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
445
- zou-0.20.44.dist-info/METADATA,sha256=81GC-qWSQIqzelfI5AxhqJkTSt9P0ti7ZUA5-ocDswc,6826
446
- zou-0.20.44.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
447
- zou-0.20.44.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
448
- zou-0.20.44.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
449
- zou-0.20.44.dist-info/RECORD,,
449
+ zou-0.20.46.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
450
+ zou-0.20.46.dist-info/METADATA,sha256=l_Jg-X9ADP-eG6swO7GtW2Sh_qR6vCTMVzrzrHmNRZI,6826
451
+ zou-0.20.46.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
452
+ zou-0.20.46.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
453
+ zou-0.20.46.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
454
+ zou-0.20.46.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5