zou 0.19.15__py3-none-any.whl → 0.20.11__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/__init__.py +10 -2
- zou/app/api.py +2 -0
- zou/app/blueprints/assets/__init__.py +22 -0
- zou/app/blueprints/assets/resources.py +241 -4
- zou/app/blueprints/auth/__init__.py +4 -0
- zou/app/blueprints/auth/resources.py +154 -22
- zou/app/blueprints/breakdown/resources.py +4 -4
- zou/app/blueprints/chats/__init__.py +22 -0
- zou/app/blueprints/chats/resources.py +199 -0
- zou/app/blueprints/comments/resources.py +36 -19
- zou/app/blueprints/crud/__init__.py +12 -0
- zou/app/blueprints/crud/attachment_file.py +14 -5
- zou/app/blueprints/crud/base.py +29 -28
- zou/app/blueprints/crud/chat.py +13 -0
- zou/app/blueprints/crud/chat_message.py +13 -0
- zou/app/blueprints/crud/comments.py +85 -29
- zou/app/blueprints/crud/custom_action.py +1 -1
- zou/app/blueprints/crud/day_off.py +47 -9
- zou/app/blueprints/crud/department.py +1 -25
- zou/app/blueprints/crud/entity.py +46 -5
- zou/app/blueprints/crud/entity_type.py +13 -1
- zou/app/blueprints/crud/event.py +1 -1
- zou/app/blueprints/crud/file_status.py +1 -1
- zou/app/blueprints/crud/metadata_descriptor.py +24 -10
- zou/app/blueprints/crud/organisation.py +22 -5
- zou/app/blueprints/crud/output_file.py +1 -1
- zou/app/blueprints/crud/output_type.py +1 -1
- zou/app/blueprints/crud/person.py +32 -24
- zou/app/blueprints/crud/playlist.py +1 -1
- zou/app/blueprints/crud/preview_background_file.py +6 -7
- zou/app/blueprints/crud/preview_file.py +1 -1
- zou/app/blueprints/crud/project.py +14 -6
- zou/app/blueprints/crud/project_status.py +1 -1
- zou/app/blueprints/crud/schedule_item.py +4 -2
- zou/app/blueprints/crud/software.py +1 -1
- zou/app/blueprints/crud/status_automation.py +1 -1
- zou/app/blueprints/crud/studio.py +33 -0
- zou/app/blueprints/crud/task.py +47 -3
- zou/app/blueprints/crud/task_status.py +1 -1
- zou/app/blueprints/crud/task_type.py +4 -4
- zou/app/blueprints/crud/working_file.py +4 -8
- zou/app/blueprints/events/resources.py +13 -12
- zou/app/blueprints/export/csv/assets.py +15 -6
- zou/app/blueprints/export/csv/edits.py +15 -5
- zou/app/blueprints/export/csv/playlists.py +1 -1
- zou/app/blueprints/export/csv/shots.py +15 -5
- zou/app/blueprints/export/csv/time_spents.py +1 -1
- zou/app/blueprints/files/resources.py +22 -23
- zou/app/blueprints/index/resources.py +38 -29
- zou/app/blueprints/news/resources.py +25 -11
- zou/app/blueprints/persons/__init__.py +5 -2
- zou/app/blueprints/persons/resources.py +126 -120
- zou/app/blueprints/previews/__init__.py +18 -8
- zou/app/blueprints/previews/resources.py +569 -328
- zou/app/blueprints/projects/resources.py +1 -1
- zou/app/blueprints/search/resources.py +18 -6
- zou/app/blueprints/shots/__init__.py +5 -0
- zou/app/blueprints/shots/resources.py +134 -4
- zou/app/blueprints/source/__init__.py +6 -6
- zou/app/blueprints/source/csv/assets.py +10 -3
- zou/app/blueprints/source/csv/base.py +1 -1
- zou/app/blueprints/source/csv/edits.py +10 -3
- zou/app/blueprints/source/csv/shots.py +10 -3
- zou/app/blueprints/source/{edl.py → otio.py} +82 -41
- zou/app/blueprints/tasks/__init__.py +3 -2
- zou/app/blueprints/tasks/resources.py +83 -52
- zou/app/blueprints/user/__init__.py +9 -0
- zou/app/blueprints/user/resources.py +170 -12
- zou/app/config.py +10 -0
- zou/app/mixin.py +6 -5
- zou/app/models/attachment_file.py +10 -4
- zou/app/models/base.py +18 -13
- zou/app/models/build_job.py +7 -4
- zou/app/models/chat.py +44 -0
- zou/app/models/chat_message.py +37 -0
- zou/app/models/comment.py +1 -0
- zou/app/models/day_off.py +3 -0
- zou/app/models/entity.py +4 -6
- zou/app/models/entity_type.py +2 -0
- zou/app/models/organisation.py +14 -15
- zou/app/models/person.py +6 -1
- zou/app/models/project.py +3 -0
- zou/app/models/search_filter.py +11 -0
- zou/app/models/search_filter_group.py +10 -0
- zou/app/models/serializer.py +17 -17
- zou/app/models/status_automation.py +2 -0
- zou/app/models/studio.py +13 -0
- zou/app/models/subscription.py +2 -2
- zou/app/models/task.py +6 -1
- zou/app/models/task_status.py +1 -0
- zou/app/models/task_type.py +1 -0
- zou/app/models/working_file.py +1 -1
- zou/app/services/assets_service.py +101 -14
- zou/app/services/auth_service.py +17 -44
- zou/app/services/breakdown_service.py +37 -5
- zou/app/services/chats_service.py +279 -0
- zou/app/services/comments_service.py +110 -65
- zou/app/services/concepts_service.py +4 -12
- zou/app/services/deletion_service.py +43 -30
- zou/app/services/edits_service.py +5 -11
- zou/app/services/emails_service.py +4 -4
- zou/app/services/entities_service.py +17 -2
- zou/app/services/events_service.py +12 -4
- zou/app/services/exception.py +5 -5
- zou/app/services/names_service.py +7 -2
- zou/app/services/news_service.py +17 -9
- zou/app/services/persons_service.py +38 -21
- zou/app/services/playlists_service.py +8 -7
- zou/app/services/preview_files_service.py +137 -10
- zou/app/services/projects_service.py +5 -14
- zou/app/services/shots_service.py +221 -49
- zou/app/services/sync_service.py +46 -42
- zou/app/services/tasks_service.py +185 -46
- zou/app/services/time_spents_service.py +67 -20
- zou/app/services/user_service.py +350 -107
- zou/app/stores/auth_tokens_store.py +2 -1
- zou/app/stores/file_store.py +18 -0
- zou/app/stores/publisher_store.py +7 -7
- zou/app/stores/queue_store.py +1 -0
- zou/app/swagger.py +36 -20
- zou/app/utils/cache.py +2 -0
- zou/app/utils/commands.py +104 -7
- zou/app/utils/csv_utils.py +1 -4
- zou/app/utils/date_helpers.py +33 -17
- zou/app/utils/dbhelpers.py +14 -1
- zou/app/utils/emails.py +2 -2
- zou/app/utils/fido.py +22 -0
- zou/app/utils/query.py +54 -6
- zou/app/utils/redis.py +11 -0
- zou/app/utils/saml.py +51 -0
- zou/app/utils/string.py +2 -0
- zou/app/utils/thumbnail.py +4 -2
- zou/cli.py +76 -18
- zou/debug.py +4 -2
- zou/event_stream.py +122 -165
- zou/job_settings.py +1 -0
- zou/migrations/env.py +0 -0
- zou/migrations/utils/base.py +6 -6
- zou/migrations/versions/1bb55759146f_add_table_studio.py +67 -0
- zou/migrations/versions/1fab8c420678_add_attachments_to_message_chats.py +56 -0
- zou/migrations/versions/23122f290ca2_add_entity_chat_models.py +149 -0
- zou/migrations/versions/32f134ff1201_add_is_shared_flag_to_filters.py +33 -0
- zou/migrations/versions/57222395f2be_add_statusautomation_import_last_revision.py +41 -0
- zou/migrations/versions/59a7445a966c_add_entity_is_shared.py +41 -0
- zou/migrations/versions/5b980f0dc365_add_comment_links.py +35 -0
- zou/migrations/versions/680c64565f9d_for_searchfiltergroup_is_shared.py +35 -0
- zou/migrations/versions/8e67c183bed7_add_preference_fields.py +71 -0
- zou/migrations/versions/92b40d79ad3f_allow_message_attachments.py +38 -0
- zou/migrations/versions/971dbf5a0faf_add_short_name_for_asset_type_entity_.py +33 -0
- zou/migrations/versions/9b85c14fa8a7_add_day_off_new_columns.py +68 -0
- zou/migrations/versions/9d3bb33c6fc6_add_department_keys_to_filter_models.py +73 -0
- zou/migrations/versions/a252a094e977_add_descriptions_for_entities_tasks_and_.py +40 -0
- zou/migrations/versions/be56dc0fb760_for_is_shared_disallow_nullable.py +102 -0
- zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py +108 -0
- zou/migrations/versions/f344b867a911_for_description_of_entity_task_working_.py +75 -0
- zou/remote/config_payload.py +2 -1
- zou/utils/movie.py +14 -4
- {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/METADATA +75 -69
- {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/RECORD +163 -134
- {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/WHEEL +1 -1
- {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/LICENSE +0 -0
- {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/entry_points.txt +0 -0
- {zou-0.19.15.dist-info → zou-0.20.11.dist-info}/top_level.txt +0 -0
zou/app/utils/redis.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from zou.app import config
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_redis_url(db_index):
|
|
5
|
+
redis_host = config.KEY_VALUE_STORE["host"]
|
|
6
|
+
redis_port = config.KEY_VALUE_STORE["port"]
|
|
7
|
+
if config.KEY_VALUE_STORE["password"]:
|
|
8
|
+
redis_password = f":{config.KEY_VALUE_STORE['password']}@"
|
|
9
|
+
else:
|
|
10
|
+
redis_password = ""
|
|
11
|
+
return f"redis://{redis_password}{redis_host}:{redis_port}/{db_index}"
|
zou/app/utils/saml.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from saml2 import (
|
|
4
|
+
BINDING_HTTP_POST,
|
|
5
|
+
BINDING_HTTP_REDIRECT,
|
|
6
|
+
)
|
|
7
|
+
from saml2.client import Saml2Client
|
|
8
|
+
from saml2.config import Config as Saml2Config
|
|
9
|
+
|
|
10
|
+
from zou.app import config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def saml_client_for(metadata_url):
|
|
14
|
+
"""
|
|
15
|
+
Given the name of an IdP, return a configuation.
|
|
16
|
+
The configuration is a hash for use by saml2.config.Config
|
|
17
|
+
"""
|
|
18
|
+
acs_url = (
|
|
19
|
+
f"{config.DOMAIN_PROTOCOL}://{config.DOMAIN_NAME}/api/auth/saml/sso"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
rv = requests.get(metadata_url)
|
|
23
|
+
|
|
24
|
+
settings = {
|
|
25
|
+
"entityid": f"{config.DOMAIN_PROTOCOL}://{config.DOMAIN_NAME}/api/auth/saml/login",
|
|
26
|
+
"metadata": {"inline": [rv.text]},
|
|
27
|
+
"service": {
|
|
28
|
+
"sp": {
|
|
29
|
+
"endpoints": {
|
|
30
|
+
"assertion_consumer_service": [
|
|
31
|
+
(acs_url, BINDING_HTTP_REDIRECT),
|
|
32
|
+
(acs_url, BINDING_HTTP_POST),
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
# Don't verify that the incoming requests originate from us via
|
|
36
|
+
# the built-in cache for authn request ids in pysaml2
|
|
37
|
+
"allow_unsolicited": True,
|
|
38
|
+
# Don't sign authn requests, since signed requests only make
|
|
39
|
+
# sense in a situation where you control both the SP and IdP
|
|
40
|
+
"authn_requests_signed": False,
|
|
41
|
+
"logout_requests_signed": True,
|
|
42
|
+
"want_assertions_signed": True,
|
|
43
|
+
"want_response_signed": False,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
spConfig = Saml2Config()
|
|
48
|
+
spConfig.load(settings)
|
|
49
|
+
spConfig.allow_unknown_attributes = True
|
|
50
|
+
saml_client = Saml2Client(config=spConfig)
|
|
51
|
+
return saml_client
|
zou/app/utils/string.py
CHANGED
zou/app/utils/thumbnail.py
CHANGED
|
@@ -76,7 +76,7 @@ def make_im_bigger_if_needed(im, size):
|
|
|
76
76
|
return im
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def fit_to_target_size(im, size):
|
|
79
|
+
def fit_to_target_size(im, size, crop=False):
|
|
80
80
|
"""
|
|
81
81
|
Make sure that the image is contained in the size given in parameter
|
|
82
82
|
(shorten width and/or height proporitionnally to expected ratio).
|
|
@@ -131,11 +131,13 @@ def turn_into_thumbnail(file_path, size=None):
|
|
|
131
131
|
return file_path
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def resize(file_path, size):
|
|
134
|
+
def resize(file_path, size, crop=False):
|
|
135
135
|
"""
|
|
136
136
|
Resize given picture
|
|
137
137
|
"""
|
|
138
138
|
im = Image.open(file_path)
|
|
139
|
+
if crop:
|
|
140
|
+
im = prepare_image_for_thumbnail(im, size)
|
|
139
141
|
im = im.resize(size, Image.Resampling.LANCZOS)
|
|
140
142
|
if im.mode == "CMYK":
|
|
141
143
|
im = im.convert("RGB")
|
zou/cli.py
CHANGED
|
@@ -30,7 +30,7 @@ def cli():
|
|
|
30
30
|
@cli.command()
|
|
31
31
|
def version():
|
|
32
32
|
"""
|
|
33
|
-
|
|
33
|
+
Return current installation version.
|
|
34
34
|
"""
|
|
35
35
|
from zou import __version__
|
|
36
36
|
|
|
@@ -39,7 +39,7 @@ def version():
|
|
|
39
39
|
|
|
40
40
|
@cli.command()
|
|
41
41
|
def init_db():
|
|
42
|
-
"
|
|
42
|
+
"Create database table (database must be created through PG client)."
|
|
43
43
|
|
|
44
44
|
print("Creating database and tables...")
|
|
45
45
|
with app.app_context():
|
|
@@ -47,6 +47,22 @@ def init_db():
|
|
|
47
47
|
print("Database and tables created.")
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
@cli.command()
|
|
51
|
+
def is_db_ready():
|
|
52
|
+
"""
|
|
53
|
+
Return a message telling whether the database is initialized or not.
|
|
54
|
+
"""
|
|
55
|
+
with app.app_context():
|
|
56
|
+
is_init = dbhelpers.is_init()
|
|
57
|
+
if is_init:
|
|
58
|
+
print("Database is initialized.")
|
|
59
|
+
else:
|
|
60
|
+
print(
|
|
61
|
+
"Database is not initialized. "
|
|
62
|
+
"Run 'zou init-db' and 'zou init-data'."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
50
66
|
@cli.command()
|
|
51
67
|
@click.option("--message", default="")
|
|
52
68
|
def migrate_db(message):
|
|
@@ -82,7 +98,7 @@ def clear_db():
|
|
|
82
98
|
|
|
83
99
|
@cli.command()
|
|
84
100
|
def reset_db():
|
|
85
|
-
"Drop all tables then
|
|
101
|
+
"Drop all tables, then recreate them."
|
|
86
102
|
with app.app_context():
|
|
87
103
|
print("Deleting database and tables...")
|
|
88
104
|
dbhelpers.drop_all()
|
|
@@ -96,7 +112,7 @@ def reset_db():
|
|
|
96
112
|
@click.option("--no-telemetry", is_flag=True, default=False)
|
|
97
113
|
def upgrade_db(no_telemetry=False):
|
|
98
114
|
"""
|
|
99
|
-
Upgrade database schema. Send anonymized statistics to our
|
|
115
|
+
Upgrade database schema. Send anonymized statistics to our telemetry
|
|
100
116
|
services (user and preview amounts). It allows us to size the Kitsu
|
|
101
117
|
community.
|
|
102
118
|
"""
|
|
@@ -140,7 +156,7 @@ def reset_migrations():
|
|
|
140
156
|
|
|
141
157
|
@cli.command()
|
|
142
158
|
@click.argument("email")
|
|
143
|
-
@click.option("--password",
|
|
159
|
+
@click.option("--password", default=None)
|
|
144
160
|
def create_admin(email, password):
|
|
145
161
|
"""
|
|
146
162
|
Create an admin user to allow usage of the API when database is empty.
|
|
@@ -149,10 +165,18 @@ def create_admin(email, password):
|
|
|
149
165
|
try:
|
|
150
166
|
person = persons_service.get_person_by_email(email)
|
|
151
167
|
if person["role"] != "admin":
|
|
152
|
-
persons_service.update_person(
|
|
168
|
+
persons_service.update_person(
|
|
169
|
+
person["id"],
|
|
170
|
+
{"role": "admin"},
|
|
171
|
+
bypass_protected_accounts=True,
|
|
172
|
+
)
|
|
153
173
|
print("Existing user's role has been upgraded to 'admin'.")
|
|
154
174
|
except PersonNotFoundException:
|
|
155
175
|
try:
|
|
176
|
+
if password is None:
|
|
177
|
+
raise click.MissingParameter(
|
|
178
|
+
param_type="option", param_hint="--password"
|
|
179
|
+
)
|
|
156
180
|
auth.validate_password(password)
|
|
157
181
|
# Allow "admin@example.com" to be invalid.
|
|
158
182
|
if email != "admin@example.com":
|
|
@@ -192,7 +216,7 @@ def clear_all_auth_tokens():
|
|
|
192
216
|
|
|
193
217
|
@cli.command()
|
|
194
218
|
def init_data():
|
|
195
|
-
"
|
|
219
|
+
"Generate minimal data set required to run Kitsu."
|
|
196
220
|
commands.init_data()
|
|
197
221
|
|
|
198
222
|
|
|
@@ -397,7 +421,7 @@ def sync_file_changes(event_source, source, logs_directory):
|
|
|
397
421
|
@click.option("--source", default="http://localhost:8080/api")
|
|
398
422
|
@click.option("--minutes", default=0)
|
|
399
423
|
@click.option("--page-size", default=300)
|
|
400
|
-
def sync_last_events(source, minutes,
|
|
424
|
+
def sync_last_events(source, minutes, limit):
|
|
401
425
|
"""
|
|
402
426
|
Retrieve last events that occured on source instance and import data related
|
|
403
427
|
to them. It expects that credentials to connect to source instance are
|
|
@@ -406,7 +430,7 @@ def sync_last_events(source, minutes, page_size):
|
|
|
406
430
|
login = os.getenv("SYNC_LOGIN")
|
|
407
431
|
password = os.getenv("SYNC_PASSWORD")
|
|
408
432
|
commands.import_last_changes_from_another_instance(
|
|
409
|
-
source, login, password, minutes=minutes,
|
|
433
|
+
source, login, password, minutes=minutes, limit=limit
|
|
410
434
|
)
|
|
411
435
|
|
|
412
436
|
|
|
@@ -414,16 +438,16 @@ def sync_last_events(source, minutes, page_size):
|
|
|
414
438
|
@click.option("--source", default="http://localhost:8080/api")
|
|
415
439
|
@click.option("--minutes", default=20)
|
|
416
440
|
@click.option("--page-size", default=50)
|
|
417
|
-
def sync_last_files(source, minutes,
|
|
441
|
+
def sync_last_files(source, minutes, limit):
|
|
418
442
|
"""
|
|
419
|
-
Retrieve last preview files and thumbnails
|
|
443
|
+
Retrieve last preview files and thumbnails uploaded on source instance.
|
|
420
444
|
It expects that credentials to connect to source instance are
|
|
421
445
|
given through SYNC_LOGIN and SYNC_PASSWORD environment variables.
|
|
422
446
|
"""
|
|
423
447
|
login = os.getenv("SYNC_LOGIN")
|
|
424
448
|
password = os.getenv("SYNC_PASSWORD")
|
|
425
449
|
commands.import_last_file_changes_from_another_instance(
|
|
426
|
-
source, login, password, minutes=minutes,
|
|
450
|
+
source, login, password, minutes=minutes, limit=limit
|
|
427
451
|
)
|
|
428
452
|
|
|
429
453
|
|
|
@@ -456,13 +480,13 @@ def upload_files_to_cloud_storage(days):
|
|
|
456
480
|
|
|
457
481
|
|
|
458
482
|
@cli.command()
|
|
459
|
-
@click.option("--
|
|
460
|
-
def clean_tasks_data(
|
|
483
|
+
@click.option("--project-id")
|
|
484
|
+
def clean_tasks_data(project_id):
|
|
461
485
|
"""
|
|
462
486
|
Reset task models data (retake count, wip start date and end date)
|
|
463
487
|
"""
|
|
464
|
-
if
|
|
465
|
-
commands.reset_tasks_data(
|
|
488
|
+
if project_id is not None:
|
|
489
|
+
commands.reset_tasks_data(project_id)
|
|
466
490
|
|
|
467
491
|
|
|
468
492
|
@cli.command()
|
|
@@ -470,7 +494,7 @@ def clean_tasks_data(projectid):
|
|
|
470
494
|
def remove_old_data(days):
|
|
471
495
|
"""
|
|
472
496
|
Remove old events, notifications and login logs older than 90 days
|
|
473
|
-
(by
|
|
497
|
+
(by default).
|
|
474
498
|
"""
|
|
475
499
|
commands.remove_old_data(days)
|
|
476
500
|
|
|
@@ -569,7 +593,7 @@ def reset_breakdown_data():
|
|
|
569
593
|
@click.option("--email", required=True)
|
|
570
594
|
@click.option("--name", required=True)
|
|
571
595
|
@click.option(
|
|
572
|
-
"--
|
|
596
|
+
"--expiration-date",
|
|
573
597
|
required=False,
|
|
574
598
|
default=None,
|
|
575
599
|
show_default=True,
|
|
@@ -593,5 +617,39 @@ def create_bot(
|
|
|
593
617
|
)
|
|
594
618
|
|
|
595
619
|
|
|
620
|
+
@cli.command()
|
|
621
|
+
@click.option(
|
|
622
|
+
"--preview-file-id",
|
|
623
|
+
required=False,
|
|
624
|
+
default=None,
|
|
625
|
+
show_default=True,
|
|
626
|
+
)
|
|
627
|
+
@click.option(
|
|
628
|
+
"--project-id",
|
|
629
|
+
required=False,
|
|
630
|
+
default=None,
|
|
631
|
+
show_default=True,
|
|
632
|
+
)
|
|
633
|
+
@click.option("--all-broken", is_flag=True, default=False, show_default=True)
|
|
634
|
+
@click.option(
|
|
635
|
+
"--all-processing", is_flag=True, default=False, show_default=True
|
|
636
|
+
)
|
|
637
|
+
def renormalize_movie_preview_files(
|
|
638
|
+
preview_file_id,
|
|
639
|
+
project_id,
|
|
640
|
+
all_broken,
|
|
641
|
+
all_processing,
|
|
642
|
+
):
|
|
643
|
+
"""
|
|
644
|
+
Renormalize all preview files.
|
|
645
|
+
"""
|
|
646
|
+
commands.renormalize_movie_preview_files(
|
|
647
|
+
preview_file_id,
|
|
648
|
+
project_id,
|
|
649
|
+
all_broken,
|
|
650
|
+
all_processing,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
|
|
596
654
|
if __name__ == "__main__":
|
|
597
655
|
cli()
|
zou/debug.py
CHANGED
|
@@ -13,6 +13,8 @@ socketio = SocketIO(app, cors_allowed_origins=[], cors_credentials=False)
|
|
|
13
13
|
|
|
14
14
|
if __name__ == "__main__":
|
|
15
15
|
print(
|
|
16
|
-
"The Kitsu API server is listening on
|
|
16
|
+
f"The Kitsu API server is listening on http://{config.DEBUG_HOST}:{config.DEBUG_PORT}"
|
|
17
|
+
)
|
|
18
|
+
socketio.run(
|
|
19
|
+
app, host=config.DEBUG_HOST, port=config.DEBUG_PORT, debug=True
|
|
17
20
|
)
|
|
18
|
-
socketio.run(app, port=config.DEBUG_PORT, debug=True)
|
zou/event_stream.py
CHANGED
|
@@ -2,25 +2,25 @@ from gevent import monkey
|
|
|
2
2
|
|
|
3
3
|
monkey.patch_all()
|
|
4
4
|
|
|
5
|
-
from flask import
|
|
5
|
+
from flask import jsonify
|
|
6
6
|
from flask_jwt_extended import (
|
|
7
7
|
get_jwt_identity,
|
|
8
8
|
jwt_required,
|
|
9
9
|
verify_jwt_in_request,
|
|
10
|
-
JWTManager,
|
|
11
10
|
)
|
|
12
11
|
from flask_socketio import SocketIO, disconnect, join_room, emit
|
|
13
|
-
|
|
14
|
-
from zou.app import config
|
|
15
|
-
from zou.app.
|
|
16
|
-
from zou.app.utils.monitoring import init_monitoring
|
|
17
|
-
from zou.app.utils.flask import ORJSONProvider
|
|
18
|
-
from zou.app.services import persons_service
|
|
12
|
+
|
|
13
|
+
from zou.app import config, app
|
|
14
|
+
from zou.app.utils.redis import get_redis_url
|
|
19
15
|
|
|
20
16
|
server_stats = {"nb_connections": 0}
|
|
21
17
|
rooms_data = {}
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
redis_url = get_redis_url(config.KV_EVENTS_DB_INDEX)
|
|
20
|
+
socketio = SocketIO(
|
|
21
|
+
logger=True, cors_allowed_origins=[], cors_credentials=False
|
|
22
|
+
)
|
|
23
|
+
socketio.init_app(app, message_queue=redis_url, async_mode="gevent")
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _get_empty_room(current_frame=0):
|
|
@@ -78,174 +78,131 @@ def _update_room_playing_status(data, room):
|
|
|
78
78
|
return room
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
@app.route("/", methods=["GET"])
|
|
82
|
+
def index():
|
|
83
|
+
return jsonify({"name": "%s Event stream" % config.APP_NAME})
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
db_index = config.KV_EVENTS_DB_INDEX
|
|
88
|
-
return "redis://%s:%s/%s" % (redis_host, redis_port, db_index)
|
|
86
|
+
@app.route("/stats", methods=["GET"])
|
|
87
|
+
def stats():
|
|
88
|
+
return jsonify(server_stats)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
@socketio.on("connect", namespace="/events")
|
|
92
|
+
def connected(_):
|
|
93
|
+
try:
|
|
94
|
+
verify_jwt_in_request()
|
|
95
|
+
server_stats["nb_connections"] += 1
|
|
96
|
+
app.logger.info("New websocket client connected")
|
|
97
|
+
except Exception:
|
|
98
|
+
app.logger.info("New websocket client failed to connect")
|
|
99
|
+
disconnect()
|
|
100
|
+
return False
|
|
92
101
|
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
@socketio.on("disconnect", namespace="/events")
|
|
104
|
+
def disconnected(_):
|
|
105
|
+
try:
|
|
106
|
+
verify_jwt_in_request()
|
|
107
|
+
user_id = get_jwt_identity()
|
|
108
|
+
# needed to be able to clear empty rooms
|
|
109
|
+
tmp_rooms_data = dict(rooms_data)
|
|
110
|
+
for room_id in tmp_rooms_data:
|
|
111
|
+
_leave_room(room_id, user_id)
|
|
112
|
+
server_stats["nb_connections"] -= 1
|
|
113
|
+
app.logger.info("Websocket client disconnected")
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@socketio.on_error("/events")
|
|
119
|
+
def on_error(error):
|
|
120
|
+
server_stats["nb_connections"] -= 1
|
|
121
|
+
if server_stats["nb_connections"] < 0:
|
|
122
|
+
server_stats["nb_connections"] = 0
|
|
123
|
+
app.logger.error(error)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.route("/rooms", methods=["GET", "POST"])
|
|
127
|
+
@jwt_required()
|
|
128
|
+
def rooms():
|
|
129
|
+
return jsonify({"name": "%s Review rooms" % config.APP_NAME})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@socketio.on("preview-room:open-playlist", namespace="/events")
|
|
133
|
+
@jwt_required()
|
|
134
|
+
def on_open_playlist(data):
|
|
135
|
+
"""
|
|
136
|
+
when a person opens the playlist page he immediately enters the
|
|
137
|
+
websocket room. This way he can see in live which people are in the
|
|
138
|
+
review room. The user still has to explicitly enter the review room
|
|
139
|
+
to actually be in sync with the other users
|
|
140
|
+
"""
|
|
141
|
+
room, room_id = _get_room_from_data(data)
|
|
142
|
+
rooms_data[room_id] = room
|
|
143
|
+
join_room(room_id)
|
|
144
|
+
emit("preview-room:room-people-updated", room, room=room_id)
|
|
98
145
|
|
|
99
|
-
@app.route("/stats", methods=["GET"])
|
|
100
|
-
def stats():
|
|
101
|
-
return jsonify(server_stats)
|
|
102
146
|
|
|
147
|
+
@socketio.on("preview-room:join", namespace="/events")
|
|
148
|
+
@jwt_required()
|
|
149
|
+
def on_join(data):
|
|
150
|
+
"""
|
|
151
|
+
When a person joins the review room, we notify all its members that a
|
|
152
|
+
new person is added to the room.
|
|
153
|
+
"""
|
|
154
|
+
user_id = get_jwt_identity()
|
|
155
|
+
room, room_id = _get_room_from_data(data)
|
|
156
|
+
if len(room["people"]) == 0:
|
|
157
|
+
_update_room_playing_status(data, room)
|
|
158
|
+
room["people"] = list(set(room["people"] + [user_id]))
|
|
159
|
+
rooms_data[room_id] = room
|
|
160
|
+
emit("preview-room:room-people-updated", room, room=room_id)
|
|
103
161
|
|
|
104
|
-
def set_application_routes(socketio, app):
|
|
105
|
-
@socketio.on("connect", namespace="/events")
|
|
106
|
-
def connected():
|
|
107
|
-
try:
|
|
108
|
-
verify_jwt_in_request()
|
|
109
|
-
server_stats["nb_connections"] += 1
|
|
110
|
-
app.logger.info("New websocket client connected")
|
|
111
|
-
except Exception:
|
|
112
|
-
app.logger.info("New websocket client failed to connect")
|
|
113
|
-
disconnect()
|
|
114
|
-
return False
|
|
115
162
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
tmp_rooms_data = dict(rooms_data)
|
|
123
|
-
for room_id in tmp_rooms_data:
|
|
124
|
-
_leave_room(room_id, user_id)
|
|
125
|
-
server_stats["nb_connections"] -= 1
|
|
126
|
-
app.logger.info("Websocket client disconnected")
|
|
127
|
-
except Exception:
|
|
128
|
-
pass
|
|
163
|
+
@socketio.on("preview-room:leave", namespace="/events")
|
|
164
|
+
@jwt_required()
|
|
165
|
+
def on_leave(data):
|
|
166
|
+
user_id = get_jwt_identity()
|
|
167
|
+
room_id = data["playlist_id"]
|
|
168
|
+
_leave_room(room_id, user_id)
|
|
129
169
|
|
|
130
|
-
@socketio.on_error("/events")
|
|
131
|
-
def on_error(error):
|
|
132
|
-
server_stats["nb_connections"] -= 1
|
|
133
|
-
if server_stats["nb_connections"] < 0:
|
|
134
|
-
server_stats["nb_connections"] = 0
|
|
135
|
-
app.logger.error(error)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def set_playlist_room_routes(socketio, app):
|
|
139
|
-
@app.route("/rooms", methods=["GET", "POST"])
|
|
140
|
-
@jwt_required()
|
|
141
|
-
def rooms():
|
|
142
|
-
return jsonify({"name": "%s Review rooms" % config.APP_NAME})
|
|
143
|
-
|
|
144
|
-
@socketio.on("preview-room:open-playlist", namespace="/events")
|
|
145
|
-
@jwt_required()
|
|
146
|
-
def on_open_playlist(data):
|
|
147
|
-
"""
|
|
148
|
-
when a person opens the playlist page he immediately enters the
|
|
149
|
-
websocket room. This way he can see in live which people are in the
|
|
150
|
-
review room. The user still has to explicitly enter the review room
|
|
151
|
-
to actually be in sync with the other users
|
|
152
|
-
"""
|
|
153
|
-
room, room_id = _get_room_from_data(data)
|
|
154
|
-
rooms_data[room_id] = room
|
|
155
|
-
join_room(room_id)
|
|
156
|
-
emit("preview-room:room-people-updated", room, room=room_id)
|
|
157
|
-
|
|
158
|
-
@socketio.on("preview-room:join", namespace="/events")
|
|
159
|
-
@jwt_required()
|
|
160
|
-
def on_join(data):
|
|
161
|
-
"""
|
|
162
|
-
When a person joins the review room, we notify all its members that a
|
|
163
|
-
new person is added to the room.
|
|
164
|
-
"""
|
|
165
|
-
user_id = get_jwt_identity()
|
|
166
|
-
room, room_id = _get_room_from_data(data)
|
|
167
|
-
if len(room["people"]) == 0:
|
|
168
|
-
_update_room_playing_status(data, room)
|
|
169
|
-
room["people"] = list(set(room["people"] + [user_id]))
|
|
170
|
-
rooms_data[room_id] = room
|
|
171
|
-
emit("preview-room:room-people-updated", room, room=room_id)
|
|
172
170
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def on_change_version(data):
|
|
209
|
-
room_id = data["playlist_id"]
|
|
210
|
-
emit("preview-room:change-version", data, room=room_id)
|
|
211
|
-
|
|
212
|
-
return app
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def create_app():
|
|
216
|
-
redis_url = get_redis_url()
|
|
217
|
-
socketio = SocketIO(
|
|
218
|
-
logger=True, cors_allowed_origins=[], cors_credentials=False
|
|
219
|
-
)
|
|
220
|
-
app = Flask(__name__)
|
|
221
|
-
app.json = ORJSONProvider(app)
|
|
222
|
-
app.config.from_object(config)
|
|
223
|
-
init_monitoring(app)
|
|
224
|
-
db = SQLAlchemy(app)
|
|
225
|
-
app.extensions["sqlalchemy"].db = db
|
|
226
|
-
set_info_routes(socketio, app)
|
|
227
|
-
set_application_routes(socketio, app)
|
|
228
|
-
set_playlist_room_routes(socketio, app)
|
|
229
|
-
socketio.init_app(app, message_queue=redis_url, async_mode="gevent")
|
|
230
|
-
return (app, socketio)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def set_auth(app):
|
|
234
|
-
jwt = JWTManager(app) # JWT auth tokens
|
|
235
|
-
|
|
236
|
-
@jwt.token_in_blocklist_loader
|
|
237
|
-
def check_if_token_is_revoked(_, payload):
|
|
238
|
-
identity_type = payload.get("identity_type")
|
|
239
|
-
if identity_type == "person":
|
|
240
|
-
return auth_tokens_store.is_revoked(payload["jti"])
|
|
241
|
-
elif identity_type in ["bot", "person_api"]:
|
|
242
|
-
return persons_service.is_jti_revoked(payload["jti"])
|
|
243
|
-
else:
|
|
244
|
-
return True
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
(app, socketio) = create_app()
|
|
248
|
-
set_auth(app)
|
|
171
|
+
@socketio.on("preview-room:update-playing-status", namespace="/events")
|
|
172
|
+
@jwt_required()
|
|
173
|
+
def on_playing_status_updated(data, only_newcomer=False):
|
|
174
|
+
room, room_id = _get_room_from_data(data)
|
|
175
|
+
rooms_data[room_id] = _update_room_playing_status(data, room)
|
|
176
|
+
event_data = {"only_newcomer": only_newcomer, **rooms_data[room_id]}
|
|
177
|
+
emit("preview-room:room-updated", event_data, room=room_id)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@socketio.on("preview-room:add-annotation", namespace="/events")
|
|
181
|
+
@jwt_required()
|
|
182
|
+
def on_add_annotation(data):
|
|
183
|
+
room_id = data["playlist_id"]
|
|
184
|
+
emit("preview-room:add-annotation", data, room=room_id)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@socketio.on("preview-room:remove-annotation", namespace="/events")
|
|
188
|
+
@jwt_required()
|
|
189
|
+
def on_remove_annotation(data):
|
|
190
|
+
room_id = data["playlist_id"]
|
|
191
|
+
emit("preview-room:remove-annotation", data, room=room_id)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@socketio.on("preview-room:update-annotation", namespace="/events")
|
|
195
|
+
@jwt_required()
|
|
196
|
+
def on_update_annotation(data):
|
|
197
|
+
room_id = data["playlist_id"]
|
|
198
|
+
emit("preview-room:update-annotation", data, room=room_id)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@socketio.on("preview-room:change-version", namespace="/events")
|
|
202
|
+
@jwt_required()
|
|
203
|
+
def on_change_version(data):
|
|
204
|
+
room_id = data["playlist_id"]
|
|
205
|
+
emit("preview-room:change-version", data, room=room_id)
|
|
249
206
|
|
|
250
207
|
|
|
251
208
|
if __name__ == "__main__":
|
zou/job_settings.py
CHANGED
zou/migrations/env.py
CHANGED
|
File without changes
|
zou/migrations/utils/base.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
|
|
3
1
|
from sqlalchemy_utils import UUIDType
|
|
4
|
-
from zou.app.utils import fields
|
|
2
|
+
from zou.app.utils import fields, date_helpers
|
|
5
3
|
import sqlalchemy as sa
|
|
6
4
|
|
|
7
5
|
|
|
@@ -11,9 +9,11 @@ class BaseMixin(object):
|
|
|
11
9
|
)
|
|
12
10
|
|
|
13
11
|
# Audit fields
|
|
14
|
-
created_at = sa.Column(
|
|
12
|
+
created_at = sa.Column(
|
|
13
|
+
sa.DateTime, default=date_helpers.get_utc_now_datetime
|
|
14
|
+
)
|
|
15
15
|
updated_at = sa.Column(
|
|
16
16
|
sa.DateTime,
|
|
17
|
-
default=
|
|
18
|
-
onupdate=
|
|
17
|
+
default=date_helpers.get_utc_now_datetime,
|
|
18
|
+
onupdate=date_helpers.get_utc_now_datetime,
|
|
19
19
|
)
|