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 +1 -1
- zou/app/api.py +1 -43
- zou/app/blueprints/crud/person.py +14 -0
- zou/app/blueprints/projects/__init__.py +5 -0
- zou/app/blueprints/projects/resources.py +32 -0
- zou/app/config.py +1 -0
- zou/app/models/plugin.py +1 -0
- zou/app/services/plugins_service.py +14 -117
- zou/app/services/time_spents_service.py +36 -0
- zou/app/utils/auth.py +6 -2
- zou/app/utils/commands.py +3 -0
- zou/app/utils/plugins.py +252 -0
- zou/cli.py +20 -7
- zou/migrations/env.py +32 -76
- zou/migrations/versions/d80f02824047_add_plugin_revision.py +67 -0
- zou/plugin_template/migrations/alembic.ini +45 -0
- zou/plugin_template/migrations/env.py +67 -0
- zou/plugin_template/migrations/script.py.mako +25 -0
- zou/plugin_template/models.py +17 -0
- zou/plugin_template/routes.py +8 -1
- {zou-0.20.44.dist-info → zou-0.20.46.dist-info}/METADATA +1 -1
- {zou-0.20.44.dist-info → zou-0.20.46.dist-info}/RECORD +26 -21
- {zou-0.20.44.dist-info → zou-0.20.46.dist-info}/WHEEL +1 -1
- {zou-0.20.44.dist-info → zou-0.20.46.dist-info}/entry_points.txt +0 -0
- {zou-0.20.44.dist-info → zou-0.20.46.dist-info}/licenses/LICENSE +0 -0
- {zou-0.20.44.dist-info → zou-0.20.46.dist-info}/top_level.txt +0 -0
zou/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.20.
|
|
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
|
|
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
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
26
|
+
def validate_email(
|
|
27
|
+
email, check_deliverability=config.MAIL_CHECK_DELIVERABILITY
|
|
28
|
+
):
|
|
27
29
|
try:
|
|
28
|
-
return email_validator.validate_email(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
8
|
+
from zou.app import db
|
|
10
9
|
|
|
11
|
-
#
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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)
|
zou/plugin_template/routes.py
CHANGED
|
@@ -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
|
-
|
|
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,11 +1,11 @@
|
|
|
1
|
-
zou/__init__.py,sha256=
|
|
2
|
-
zou/cli.py,sha256=
|
|
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=
|
|
8
|
-
zou/app/config.py,sha256=
|
|
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=
|
|
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=
|
|
100
|
-
zou/app/blueprints/projects/resources.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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/
|
|
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.
|
|
445
|
-
zou-0.20.
|
|
446
|
-
zou-0.20.
|
|
447
|
-
zou-0.20.
|
|
448
|
-
zou-0.20.
|
|
449
|
-
zou-0.20.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|