zou 1.0.4__py3-none-any.whl → 1.0.5__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 (40) hide show
  1. zou/__init__.py +1 -1
  2. zou/app/blueprints/crud/person.py +1 -0
  3. zou/app/blueprints/crud/task_type.py +10 -0
  4. zou/app/blueprints/files/resources.py +2 -0
  5. zou/app/blueprints/index/resources.py +76 -66
  6. zou/app/blueprints/news/resources.py +2 -2
  7. zou/app/blueprints/playlists/resources.py +8 -2
  8. zou/app/blueprints/previews/resources.py +11 -2
  9. zou/app/config.py +2 -0
  10. zou/app/file_trees/default.json +6 -0
  11. zou/app/file_trees/simple.json +6 -0
  12. zou/app/mixin.py +9 -0
  13. zou/app/models/plugin.py +21 -2
  14. zou/app/models/preview_file.py +2 -0
  15. zou/app/services/comments_service.py +2 -1
  16. zou/app/services/deletion_service.py +14 -5
  17. zou/app/services/file_tree_service.py +10 -5
  18. zou/app/services/notifications_service.py +11 -2
  19. zou/app/services/persons_service.py +1 -0
  20. zou/app/services/plugins_service.py +83 -11
  21. zou/app/services/schedule_service.py +2 -1
  22. zou/app/services/tasks_service.py +1 -1
  23. zou/app/services/user_service.py +3 -0
  24. zou/app/utils/commands.py +1 -1
  25. zou/app/utils/plugins.py +114 -10
  26. zou/cli.py +15 -5
  27. zou/migrations/versions/12208e50bf18_add_json_data_field_to_preview_files.py +38 -0
  28. zou/migrations/versions/35ebb38695cd_add_icon_field_to_plugins.py +36 -0
  29. zou/migrations/versions/9a9df20ea5a7_add_frontend_options_to_plugins.py +40 -0
  30. zou/migrations/versions/a0f668430352_add_created_by_field_to_playlists.py +16 -8
  31. zou/plugin_template/__init__.py +2 -13
  32. zou/plugin_template/migrations/env.py +9 -0
  33. zou/plugin_template/models.py +2 -2
  34. zou/plugin_template/{routes.py → resources.py} +4 -0
  35. {zou-1.0.4.dist-info → zou-1.0.5.dist-info}/METADATA +1 -1
  36. {zou-1.0.4.dist-info → zou-1.0.5.dist-info}/RECORD +40 -37
  37. {zou-1.0.4.dist-info → zou-1.0.5.dist-info}/WHEEL +0 -0
  38. {zou-1.0.4.dist-info → zou-1.0.5.dist-info}/entry_points.txt +0 -0
  39. {zou-1.0.4.dist-info → zou-1.0.5.dist-info}/licenses/LICENSE +0 -0
  40. {zou-1.0.4.dist-info → zou-1.0.5.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import semver
2
+ import shutil
2
3
  from pathlib import Path
3
4
 
4
5
  from zou.app import config, db
@@ -9,16 +10,32 @@ from zou.app.utils.plugins import (
9
10
  downgrade_plugin_migrations,
10
11
  uninstall_plugin_files,
11
12
  install_plugin_files,
13
+ clone_git_repo,
12
14
  )
13
15
 
14
16
 
15
17
  def install_plugin(path, force=False):
16
18
  """
17
- Install a plugin.
19
+ Install a plugin: create folder, copy files, run migrations.
20
+ Supports local paths, zip files, and git repository URLs.
18
21
  """
19
- path = Path(path)
20
- if not path.exists():
21
- raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
22
+ is_git_url = (
23
+ path.startswith("http://")
24
+ or path.startswith("https://")
25
+ or path.startswith("git://")
26
+ or path.startswith("ssh://")
27
+ or path.startswith("git@")
28
+ )
29
+
30
+ temp_dir = None
31
+ if is_git_url:
32
+ cloned_path = clone_git_repo(path)
33
+ temp_dir = cloned_path.parent
34
+ path = cloned_path
35
+ else:
36
+ path = Path(path)
37
+ if not path.exists():
38
+ raise FileNotFoundError(f"Plugin path '{path}' does not exist.")
22
39
 
23
40
  manifest = PluginManifest.from_plugin_path(path)
24
41
  plugin = Plugin.query.filter_by(plugin_id=manifest.id).one_or_none()
@@ -27,32 +44,46 @@ def install_plugin(path, force=False):
27
44
  if plugin:
28
45
  current = semver.Version.parse(plugin.version)
29
46
  new = semver.Version.parse(str(manifest.version))
47
+ print(
48
+ f"[Plugins] Upgrading plugin {manifest.id} from version {current} to {new}..."
49
+ )
30
50
  if not force and new <= current:
31
- raise ValueError(
32
- f"Plugin version {new} is not newer than {current}."
33
- )
51
+ print(f"⚠️ Plugin version {new} is not newer than {current}.")
34
52
  plugin.update_no_commit(manifest.to_model_dict())
53
+ print(f"[Plugins] Plugin {manifest.id} upgraded.")
35
54
  else:
55
+ print(f"[Plugins] Installing plugin {manifest.id}...")
36
56
  plugin = Plugin.create_no_commit(**manifest.to_model_dict())
57
+ print(f"[Plugins] Plugin {manifest.id} installed.")
37
58
 
59
+ print(f"[Plugins] Running database migrations for {manifest.id}...")
38
60
  plugin_path = install_plugin_files(
39
61
  path, Path(config.PLUGIN_FOLDER) / manifest.id
40
62
  )
41
63
  run_plugin_migrations(plugin_path, plugin)
64
+ print(f"[Plugins] Database migrations for {manifest.id} applied.")
42
65
  except Exception:
43
- uninstall_plugin_files(manifest.id)
44
- db.session.rollback()
45
- db.session.remove()
66
+ print(
67
+ f"❌ [Plugins] An error occurred while installing/updating {manifest.id}..."
68
+ )
46
69
  raise
47
70
 
48
71
  Plugin.commit()
72
+ print_added_routes(plugin, plugin_path)
73
+
74
+ if is_git_url:
75
+ if temp_dir and temp_dir.exists():
76
+ shutil.rmtree(temp_dir)
77
+
49
78
  return plugin.serialize()
50
79
 
51
80
 
52
81
  def uninstall_plugin(plugin_id):
53
82
  """
54
- Uninstall a plugin.
83
+ Uninstall a plugin: downgrade migrations, remove files,
84
+ delete from database and remove folder.
55
85
  """
86
+ print(f"[Plugins] Uninstalling plugin {plugin_id}...")
56
87
  plugin_path = Path(config.PLUGIN_FOLDER) / plugin_id
57
88
  downgrade_plugin_migrations(plugin_path)
58
89
  installed = uninstall_plugin_files(plugin_path)
@@ -63,4 +94,45 @@ def uninstall_plugin(plugin_id):
63
94
 
64
95
  if not installed:
65
96
  raise ValueError(f"Plugin '{plugin_id}' is not installed.")
97
+
98
+ print(f"[Plugins] Plugin {plugin_id} uninstalled.")
66
99
  return True
100
+
101
+
102
+ def print_added_routes(plugin, plugin_path):
103
+ """
104
+ Print the added routes for a plugin.
105
+ """
106
+ import importlib
107
+ import sys
108
+
109
+ print(f"[Plugins] Routes added by {plugin.plugin_id}:")
110
+ plugin_path = Path(plugin_path)
111
+
112
+ plugin_folder = plugin_path.parent
113
+ abs_plugin_path = str(plugin_folder.absolute())
114
+ if abs_plugin_path not in sys.path:
115
+ sys.path.insert(0, abs_plugin_path)
116
+
117
+ try:
118
+ plugin_module = importlib.import_module(plugin.plugin_id)
119
+ if hasattr(plugin_module, "routes"):
120
+ routes = plugin_module.routes
121
+ for route in routes:
122
+ print(f" - /plugins/{plugin.plugin_id}{route[0]}")
123
+ else:
124
+ print(" (No routes variable found in plugin)")
125
+ except ImportError as e:
126
+ print(f" ⚠️ Could not import plugin module: {e}")
127
+ finally:
128
+ if abs_plugin_path in sys.path:
129
+ sys.path.remove(abs_plugin_path)
130
+
131
+ print("--------------------------------")
132
+
133
+
134
+ def get_plugins():
135
+ """
136
+ Get all plugins.
137
+ """
138
+ return [plugin.present() for plugin in Plugin.query.all()]
@@ -55,7 +55,8 @@ def get_task_types_schedule_items(project_id):
55
55
  task_types = [
56
56
  task_type
57
57
  for task_type in task_types
58
- if task_type["for_entity"] in ["Asset", "Shot"]
58
+ if task_type["for_entity"]
59
+ in ["Asset", "Shot", "Sequence", "Episode", "Edit"]
59
60
  ]
60
61
  task_type_map = base_service.get_model_map_from_array(task_types)
61
62
  schedule_items = set(
@@ -1815,7 +1815,7 @@ def reset_task_data(task_id):
1815
1815
  if task_status_is_wip and real_start_date is None:
1816
1816
  real_start_date = comment.created_at
1817
1817
 
1818
- if task_status_is_feedback_request:
1818
+ if task_status_is_feedback_request and end_date is None:
1819
1819
  end_date = comment.created_at
1820
1820
 
1821
1821
  print("ok", task_status_is_done)
@@ -15,6 +15,8 @@ from zou.app.models.search_filter_group import SearchFilterGroup
15
15
  from zou.app.models.task import Task
16
16
  from zou.app.models.task_type import TaskType
17
17
 
18
+ from zou.app.services import plugins_service
19
+
18
20
 
19
21
  from zou.app.services import (
20
22
  assets_service,
@@ -1670,6 +1672,7 @@ def get_context():
1670
1672
  "search_filters": get_filters(),
1671
1673
  "search_filter_groups": get_filter_groups(),
1672
1674
  "preview_background_files": files_service.get_preview_background_files(),
1675
+ "plugins": plugins_service.get_plugins(),
1673
1676
  }
1674
1677
 
1675
1678
  if permissions.has_admin_permissions():
zou/app/utils/commands.py CHANGED
@@ -848,7 +848,6 @@ def list_plugins(output_format, verbose, filter_field, filter_value):
848
848
  with app.app_context():
849
849
  query = Plugin.query
850
850
 
851
- # Apply filter if needed
852
851
  if filter_field and filter_value:
853
852
  if filter_field == "maintainer":
854
853
  query = query.filter(
@@ -881,6 +880,7 @@ def list_plugins(output_format, verbose, filter_field, filter_value):
881
880
  if verbose:
882
881
  plugin_data["Description"] = plugin.description or "-"
883
882
  plugin_data["Website"] = plugin.website or "-"
883
+ plugin_data["Icon"] = plugin.icon or "-"
884
884
  plugin_data["Revision"] = plugin.revision or "-"
885
885
  plugin_data["Installation Date"] = plugin.created_at
886
886
  plugin_data["Last Update"] = plugin.updated_at
zou/app/utils/plugins.py CHANGED
@@ -1,5 +1,3 @@
1
- import tomlkit
2
- import semver
3
1
  import email.utils
4
2
  import spdx_license_list
5
3
  import zipfile
@@ -7,17 +5,52 @@ import importlib
7
5
  import importlib.util
8
6
  import sys
9
7
  import os
8
+ import tomlkit
10
9
  import traceback
10
+ import semver
11
11
  import shutil
12
+ import subprocess
13
+ import tempfile
12
14
 
13
- from pathlib import Path
14
- from flask import current_app
15
15
  from alembic import command
16
16
  from alembic.config import Config
17
+ from collections.abc import MutableMapping
18
+ from flask import Blueprint, current_app
19
+ from flask_restful import Resource
20
+ from pathlib import Path
17
21
 
22
+ from zou.app.utils.api import configure_api_from_blueprint
18
23
 
19
- from pathlib import Path
20
- from collections.abc import MutableMapping
24
+ from flask import send_from_directory, abort, current_app
25
+
26
+
27
+ class StaticResource(Resource):
28
+
29
+ plugin_id = None
30
+
31
+ def get(self, filename="index.html"):
32
+
33
+ print(self.plugin_id)
34
+ static_folder = (
35
+ Path(current_app.config.get("PLUGIN_FOLDER", "plugins"))
36
+ / self.plugin_id
37
+ / "frontend"
38
+ / "dist"
39
+ )
40
+
41
+ if filename == "":
42
+ filename = "index.html"
43
+
44
+ file_path = static_folder / filename
45
+ if not file_path.exists() or not file_path.is_file():
46
+ abort(404)
47
+
48
+ if filename == "":
49
+ filename = "index.html"
50
+
51
+ return send_from_directory(
52
+ str(static_folder), filename, conditional=True, max_age=0
53
+ )
21
54
 
22
55
 
23
56
  class PluginManifest(MutableMapping):
@@ -57,6 +90,11 @@ class PluginManifest(MutableMapping):
57
90
  self.data["maintainer_name"] = name
58
91
  self.data["maintainer_email"] = email_addr
59
92
 
93
+ if "frontend_project_enabled" not in self.data:
94
+ self.data["frontend_project_enabled"] = False
95
+ if "frontend_studio_enabled" not in self.data:
96
+ self.data["frontend_studio_enabled"] = False
97
+
60
98
  def to_model_dict(self):
61
99
  return {
62
100
  "plugin_id": self.data["id"],
@@ -67,6 +105,13 @@ class PluginManifest(MutableMapping):
67
105
  "maintainer_email": self.data.get("maintainer_email"),
68
106
  "website": self.data.get("website"),
69
107
  "license": self.data["license"],
108
+ "frontend_project_enabled": self.data.get(
109
+ "frontend_project_enabled", False
110
+ ),
111
+ "frontend_studio_enabled": self.data.get(
112
+ "frontend_studio_enabled", False
113
+ ),
114
+ "icon": self.data.get("icon", ""),
70
115
  }
71
116
 
72
117
  def __getitem__(self, key):
@@ -106,10 +151,16 @@ def load_plugin(app, plugin_path, init_plugin=True):
106
151
  """
107
152
  plugin_path = Path(plugin_path)
108
153
  manifest = PluginManifest.from_plugin_path(plugin_path)
109
-
110
154
  plugin_module = importlib.import_module(manifest["id"])
111
- if init_plugin and hasattr(plugin_module, "init_plugin"):
112
- plugin_module.init_plugin(app, manifest)
155
+
156
+ if not hasattr(plugin_module, "routes"):
157
+ raise Exception(f"Plugin {manifest['id']} has no routes.")
158
+
159
+ routes = plugin_module.routes
160
+ add_static_routes(manifest, routes)
161
+ blueprint = Blueprint(manifest["id"], manifest["id"])
162
+ configure_api_from_blueprint(blueprint, routes)
163
+ app.register_blueprint(blueprint, url_prefix=f"/plugins/{manifest['id']}")
113
164
 
114
165
  return plugin_module
115
166
 
@@ -270,6 +321,7 @@ def create_plugin_skeleton(
270
321
  maintainer=None,
271
322
  website=None,
272
323
  license=None,
324
+ icon=None,
273
325
  force=False,
274
326
  ):
275
327
  plugin_template_path = (
@@ -301,7 +353,8 @@ def create_plugin_skeleton(
301
353
  manifest.website = website
302
354
  if license:
303
355
  manifest.license = license
304
-
356
+ if icon:
357
+ manifest.icon = icon
305
358
  manifest.validate()
306
359
  manifest.write_to_path(plugin_path)
307
360
 
@@ -338,3 +391,54 @@ def uninstall_plugin_files(plugin_path):
338
391
  shutil.rmtree(plugin_path)
339
392
  return True
340
393
  return False
394
+
395
+
396
+ def clone_git_repo(git_url, temp_dir=None):
397
+ """
398
+ Clone a git repository to a temporary directory.
399
+ Returns the path to the cloned directory.
400
+ """
401
+ if temp_dir is None:
402
+ temp_dir = tempfile.mkdtemp(prefix="zou_plugin_")
403
+
404
+ temp_dir = Path(temp_dir)
405
+ repo_name = git_url.rstrip("/").split("/")[-1].replace(".git", "")
406
+ clone_path = temp_dir / repo_name
407
+
408
+ print(f"[Plugins] Cloning {git_url}...")
409
+
410
+ try:
411
+ subprocess.run(
412
+ ["git", "clone", git_url, str(clone_path)],
413
+ check=True,
414
+ capture_output=True,
415
+ timeout=300,
416
+ )
417
+ print(f"[Plugins] Successfully cloned {git_url}")
418
+ return clone_path
419
+ except subprocess.CalledProcessError as e:
420
+ error_msg = e.stderr.decode() if e.stderr else str(e)
421
+ raise ValueError(f"Failed to clone repository {git_url}: {error_msg}")
422
+ except FileNotFoundError:
423
+ raise ValueError(
424
+ "git is not available. Please install git to clone repositories."
425
+ )
426
+
427
+
428
+ def add_static_routes(manifest, routes):
429
+ """
430
+ Add static routes to the manifest.
431
+ """
432
+
433
+ class PluginStaticResource(StaticResource):
434
+
435
+ def __init__(self):
436
+ self.plugin_id = manifest.id
437
+ super().__init__()
438
+
439
+ if (
440
+ manifest["frontend_project_enabled"]
441
+ or manifest["frontend_studio_enabled"]
442
+ ):
443
+ routes.append((f"/frontend/<path:filename>", PluginStaticResource))
444
+ routes.append((f"/frontend", PluginStaticResource))
zou/cli.py CHANGED
@@ -660,6 +660,7 @@ def renormalize_movie_preview_files(
660
660
  @click.option(
661
661
  "--path",
662
662
  required=True,
663
+ help="Plugin path: local directory, zip file, or git repository URL",
663
664
  )
664
665
  @click.option(
665
666
  "--force",
@@ -669,11 +670,12 @@ def renormalize_movie_preview_files(
669
670
  )
670
671
  def install_plugin(path, force=False):
671
672
  """
672
- Install a plugin.
673
+ Install a plugin and apply the migrations.
674
+ Supports local paths, zip files, and git repository URLs.
673
675
  """
674
676
  with app.app_context():
675
677
  plugins_service.install_plugin(path, force)
676
- print(f"Plugin {path} installed. Restart the server to apply changes.")
678
+ print(f"Plugin {path} installed. Restart the server to apply changes.")
677
679
 
678
680
 
679
681
  @cli.command()
@@ -687,7 +689,7 @@ def uninstall_plugin(id):
687
689
  """
688
690
  with app.app_context():
689
691
  plugins_service.uninstall_plugin(id)
690
- print(f"Plugin {id} uninstalled.")
692
+ print(f"Plugin {id} uninstalled. Restart the server to apply changes.")
691
693
 
692
694
 
693
695
  @cli.command()
@@ -730,6 +732,12 @@ def uninstall_plugin(id):
730
732
  default="GPL-3.0-only",
731
733
  show_default=True,
732
734
  )
735
+ @click.option(
736
+ "--icon",
737
+ help="Plugin icon (lucide-vue icon name).",
738
+ default=None,
739
+ show_default=True,
740
+ )
733
741
  @click.option(
734
742
  "--force",
735
743
  is_flag=True,
@@ -745,10 +753,11 @@ def create_plugin_skeleton(
745
753
  maintainer,
746
754
  website,
747
755
  license,
756
+ icon,
748
757
  force=False,
749
758
  ):
750
759
  """
751
- Create a plugin skeleton.
760
+ Create a plugin template in the given path.
752
761
  """
753
762
  plugin_path = plugin_utils.create_plugin_skeleton(
754
763
  path,
@@ -759,9 +768,10 @@ def create_plugin_skeleton(
759
768
  maintainer,
760
769
  website,
761
770
  license,
771
+ icon,
762
772
  force,
763
773
  )
764
- print(f"Plugin skeleton created in '{plugin_path}'.")
774
+ print(f"Plugin file tree skeleton created in '{plugin_path}'.")
765
775
 
766
776
 
767
777
  @cli.command()
@@ -0,0 +1,38 @@
1
+ """add JSON data field to preview files
2
+
3
+ Revision ID: 12208e50bf18
4
+ Revises: a0f668430352
5
+ Create Date: 2025-12-19 11:27:57.477880
6
+
7
+ """
8
+
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+ import sqlalchemy_utils
12
+ from sqlalchemy.dialects import postgresql
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision = "12208e50bf18"
16
+ down_revision = "a0f668430352"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade():
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ with op.batch_alter_table("preview_file", schema=None) as batch_op:
24
+ batch_op.add_column(
25
+ sa.Column(
26
+ "data", postgresql.JSONB(astext_type=sa.Text()), nullable=True
27
+ )
28
+ )
29
+
30
+ # ### end Alembic commands ###
31
+
32
+
33
+ def downgrade():
34
+ # ### commands auto generated by Alembic - please adjust! ###
35
+ with op.batch_alter_table("preview_file", schema=None) as batch_op:
36
+ batch_op.drop_column("data")
37
+
38
+ # ### end Alembic commands ###
@@ -0,0 +1,36 @@
1
+ """add icon field to plugins
2
+
3
+ Revision ID: 35ebb38695cd
4
+ Revises: 9a9df20ea5a7
5
+ Create Date: 2025-12-24 11:39:42.229109
6
+
7
+ """
8
+
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+ import sqlalchemy_utils
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision = "35ebb38695cd"
16
+ down_revision = "9a9df20ea5a7"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade():
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ with op.batch_alter_table("plugin", schema=None) as batch_op:
24
+ batch_op.add_column(
25
+ sa.Column("icon", sa.String(length=255), nullable=True)
26
+ )
27
+
28
+ # ### end Alembic commands ###
29
+
30
+
31
+ def downgrade():
32
+ # ### commands auto generated by Alembic - please adjust! ###
33
+ with op.batch_alter_table("plugin", schema=None) as batch_op:
34
+ batch_op.drop_column("icon")
35
+
36
+ # ### end Alembic commands ###
@@ -0,0 +1,40 @@
1
+ """add frontend options to plugins
2
+
3
+ Revision ID: 9a9df20ea5a7
4
+ Revises: 12208e50bf18
5
+ Create Date: 2025-12-23 00:47:50.621868
6
+
7
+ """
8
+
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+ import sqlalchemy_utils
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision = "9a9df20ea5a7"
16
+ down_revision = "12208e50bf18"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade():
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ with op.batch_alter_table("plugin", schema=None) as batch_op:
24
+ batch_op.add_column(
25
+ sa.Column("frontend_project_enabled", sa.Boolean(), nullable=True)
26
+ )
27
+ batch_op.add_column(
28
+ sa.Column("frontend_studio_enabled", sa.Boolean(), nullable=True)
29
+ )
30
+
31
+ # ### end Alembic commands ###
32
+
33
+
34
+ def downgrade():
35
+ # ### commands auto generated by Alembic - please adjust! ###
36
+ with op.batch_alter_table("plugin", schema=None) as batch_op:
37
+ batch_op.drop_column("frontend_studio_enabled")
38
+ batch_op.drop_column("frontend_project_enabled")
39
+
40
+ # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: a1b2c3d4e5f6
5
5
  Create Date: 2025-12-03 12:10:52.626583
6
6
 
7
7
  """
8
+
8
9
  from alembic import op
9
10
  import sqlalchemy as sa
10
11
  import sqlalchemy_utils
@@ -12,25 +13,32 @@ import sqlalchemy_utils
12
13
  import uuid
13
14
 
14
15
  # revision identifiers, used by Alembic.
15
- revision = 'a0f668430352'
16
- down_revision = 'a1b2c3d4e5f6'
16
+ revision = "a0f668430352"
17
+ down_revision = "a1b2c3d4e5f6"
17
18
  branch_labels = None
18
19
  depends_on = None
19
20
 
20
21
 
21
22
  def upgrade():
22
23
  # ### commands auto generated by Alembic - please adjust! ###
23
- with op.batch_alter_table('playlist', schema=None) as batch_op:
24
- batch_op.add_column(sa.Column('created_by', sqlalchemy_utils.types.uuid.UUIDType(binary=False), default=uuid.uuid4, nullable=True))
25
- batch_op.create_foreign_key(None, 'person', ['created_by'], ['id'])
24
+ with op.batch_alter_table("playlist", schema=None) as batch_op:
25
+ batch_op.add_column(
26
+ sa.Column(
27
+ "created_by",
28
+ sqlalchemy_utils.types.uuid.UUIDType(binary=False),
29
+ default=uuid.uuid4,
30
+ nullable=True,
31
+ )
32
+ )
33
+ batch_op.create_foreign_key(None, "person", ["created_by"], ["id"])
26
34
 
27
35
  # ### end Alembic commands ###
28
36
 
29
37
 
30
38
  def downgrade():
31
39
  # ### commands auto generated by Alembic - please adjust! ###
32
- with op.batch_alter_table('playlist', schema=None) as batch_op:
33
- batch_op.drop_constraint(None, type_='foreignkey')
34
- batch_op.drop_column('created_by')
40
+ with op.batch_alter_table("playlist", schema=None) as batch_op:
41
+ batch_op.drop_constraint(None, type_="foreignkey")
42
+ batch_op.drop_column("created_by")
35
43
 
36
44
  # ### end Alembic commands ###
@@ -1,18 +1,7 @@
1
- from . import routes
2
- from flask import Blueprint
3
- from zou.app.utils.api import configure_api_from_blueprint
1
+ from . import resources
4
2
 
5
3
 
6
- def init_plugin(app, manifest):
7
- """
8
- Init the plugin.
9
- """
10
- app.logger.info("Loading plugin...")
11
- routes_tuples = [(f"/hello-world", routes.HelloWorld)]
12
-
13
- blueprint = Blueprint(manifest["id"], manifest["id"])
14
- configure_api_from_blueprint(blueprint, routes_tuples)
15
- app.register_blueprint(blueprint, url_prefix=f"/{manifest['id']}")
4
+ routes = [(f"/hello-world", resources.HelloWorld)]
16
5
 
17
6
 
18
7
  def pre_install(manifest):
@@ -1,11 +1,14 @@
1
1
  import importlib.util
2
2
  import logging
3
3
  import sys
4
+
4
5
  from alembic import context
5
6
  from sqlalchemy import create_engine, pool
7
+
6
8
  from pathlib import Path
7
9
  from logging.config import fileConfig
8
10
 
11
+ from zou.app import db
9
12
  from zou.app.utils.plugins import PluginManifest
10
13
 
11
14
  plugin_path = Path(__file__).resolve().parents[1]
@@ -18,6 +21,12 @@ module = importlib.util.module_from_spec(spec)
18
21
  sys.modules[module_name] = module
19
22
  spec.loader.exec_module(module)
20
23
 
24
+ # Add zou tables
25
+ module.plugin_metadata.tables = {
26
+ **db.metadata.tables,
27
+ **module.plugin_metadata.tables,
28
+ }
29
+
21
30
  # Database URL (passed by Alembic)
22
31
  config = context.config
23
32
  url = config.get_main_option("sqlalchemy.url")
@@ -10,8 +10,8 @@ PluginBase = declarative_base(metadata=plugin_metadata)
10
10
 
11
11
  class Count(PluginBase, BaseMixin, SerializerMixin):
12
12
  """
13
- Describe a plugin.
13
+ A simple model to keep track of a count.
14
14
  """
15
15
 
16
- __tablename__ = "toto_count"
16
+ __tablename__ = "hello_count"
17
17
  count = Column(Integer, nullable=False, default=0)
@@ -1,8 +1,12 @@
1
1
  from flask_restful import Resource
2
+ from flask_jwt_extended import jwt_required
3
+
2
4
  from .models import Count
3
5
 
4
6
 
5
7
  class HelloWorld(Resource):
8
+
9
+ @jwt_required()
6
10
  def get(self):
7
11
  if not Count.query.first():
8
12
  c = Count.create()