zou 0.19.49__py3-none-any.whl → 0.19.51__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 +8 -0
- zou/app/blueprints/auth/__init__.py +4 -0
- zou/app/blueprints/auth/resources.py +107 -1
- zou/app/blueprints/crud/entity.py +42 -1
- zou/app/blueprints/crud/task.py +41 -1
- zou/app/blueprints/index/resources.py +14 -14
- zou/app/blueprints/previews/resources.py +1 -0
- zou/app/blueprints/shots/resources.py +50 -1
- zou/app/blueprints/tasks/resources.py +22 -2
- zou/app/config.py +4 -0
- zou/app/services/auth_service.py +7 -20
- zou/app/services/persons_service.py +5 -2
- zou/app/services/tasks_service.py +21 -1
- zou/app/utils/dbhelpers.py +14 -1
- zou/app/utils/fido.py +22 -0
- zou/app/utils/saml.py +52 -0
- zou/cli.py +27 -2
- zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py +63 -0
- {zou-0.19.49.dist-info → zou-0.19.51.dist-info}/METADATA +16 -16
- {zou-0.19.49.dist-info → zou-0.19.51.dist-info}/RECORD +25 -23
- {zou-0.19.49.dist-info → zou-0.19.51.dist-info}/WHEEL +1 -1
- {zou-0.19.49.dist-info → zou-0.19.51.dist-info}/LICENSE +0 -0
- {zou-0.19.49.dist-info → zou-0.19.51.dist-info}/entry_points.txt +0 -0
- {zou-0.19.49.dist-info → zou-0.19.51.dist-info}/top_level.txt +0 -0
zou/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.19.
|
|
1
|
+
__version__ = "0.19.51"
|
zou/app/__init__.py
CHANGED
|
@@ -41,6 +41,9 @@ from zou.app.utils.flask import (
|
|
|
41
41
|
wrong_auth_handler,
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
+
from zou.app.utils.saml import saml_client_for
|
|
45
|
+
from zou.app.utils.fido import get_fido_server
|
|
46
|
+
|
|
44
47
|
app = Flask(__name__)
|
|
45
48
|
app.json = ORJSONProvider(app)
|
|
46
49
|
app.request_class.user_agent_class = ParsedUserAgent
|
|
@@ -64,6 +67,11 @@ swagger = Swagger(
|
|
|
64
67
|
app, template=swagger.swagger_template, config=swagger.swagger_config
|
|
65
68
|
)
|
|
66
69
|
|
|
70
|
+
if config.SAML_ENABLED:
|
|
71
|
+
app.extensions["saml_client"] = saml_client_for(config.SAML_METADATA_URL)
|
|
72
|
+
|
|
73
|
+
app.extensions["fido_server"] = get_fido_server()
|
|
74
|
+
|
|
67
75
|
|
|
68
76
|
@app.teardown_appcontext
|
|
69
77
|
def shutdown_session(exception=None):
|
|
@@ -13,6 +13,8 @@ from zou.app.blueprints.auth.resources import (
|
|
|
13
13
|
RegistrationResource,
|
|
14
14
|
ResetPasswordResource,
|
|
15
15
|
TOTPResource,
|
|
16
|
+
SAMLSSOResource,
|
|
17
|
+
SAMLLoginResource,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
routes = [
|
|
@@ -27,6 +29,8 @@ routes = [
|
|
|
27
29
|
("/auth/email-otp", EmailOTPResource),
|
|
28
30
|
("/auth/recovery-codes", RecoveryCodesResource),
|
|
29
31
|
("/auth/fido", FIDOResource),
|
|
32
|
+
("/auth/saml/sso", SAMLSSOResource),
|
|
33
|
+
("/auth/saml/login", SAMLLoginResource),
|
|
30
34
|
]
|
|
31
35
|
|
|
32
36
|
blueprint = Blueprint("auth", "auth")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import urllib.parse
|
|
2
2
|
|
|
3
|
-
from flask import request, jsonify, current_app
|
|
3
|
+
from flask import request, jsonify, current_app, redirect, make_response
|
|
4
4
|
from flask_restful import Resource
|
|
5
5
|
from flask_principal import (
|
|
6
6
|
Identity,
|
|
@@ -19,6 +19,7 @@ from flask_jwt_extended import (
|
|
|
19
19
|
|
|
20
20
|
from sqlalchemy.exc import OperationalError, TimeoutError
|
|
21
21
|
from babel.dates import format_datetime
|
|
22
|
+
from saml2 import entity
|
|
22
23
|
|
|
23
24
|
from zou.app import app, config
|
|
24
25
|
from zou.app.mixin import ArgsMixin
|
|
@@ -1331,3 +1332,108 @@ class RecoveryCodesResource(Resource, ArgsMixin):
|
|
|
1331
1332
|
},
|
|
1332
1333
|
400,
|
|
1333
1334
|
)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
class SAMLSSOResource(Resource, ArgsMixin):
|
|
1338
|
+
"""
|
|
1339
|
+
Resource to allow a user to login with SAML SSO.
|
|
1340
|
+
"""
|
|
1341
|
+
|
|
1342
|
+
def post(self):
|
|
1343
|
+
"""
|
|
1344
|
+
Resource to allow a user to login with SAML SSO.
|
|
1345
|
+
---
|
|
1346
|
+
description: ""
|
|
1347
|
+
tags:
|
|
1348
|
+
- Authentication
|
|
1349
|
+
responses:
|
|
1350
|
+
302:
|
|
1351
|
+
description: Login successful, redirect to the home page.
|
|
1352
|
+
400:
|
|
1353
|
+
description: Wrong parameter
|
|
1354
|
+
"""
|
|
1355
|
+
if not config.SAML_ENABLED:
|
|
1356
|
+
return {"error": "SAML is not enabled."}, 400
|
|
1357
|
+
authn_response = current_app.extensions[
|
|
1358
|
+
"saml_client"
|
|
1359
|
+
].parse_authn_request_response(
|
|
1360
|
+
request.form["SAMLResponse"], entity.BINDING_HTTP_POST
|
|
1361
|
+
)
|
|
1362
|
+
authn_response.get_identity()
|
|
1363
|
+
email = authn_response.get_subject().text
|
|
1364
|
+
person_info = {
|
|
1365
|
+
k: v if not isinstance(v, list) else " ".join(v)
|
|
1366
|
+
for k, v in authn_response.ava.items()
|
|
1367
|
+
}
|
|
1368
|
+
try:
|
|
1369
|
+
user = persons_service.get_person_by_email(email)
|
|
1370
|
+
for k, v in person_info.items():
|
|
1371
|
+
if user.get(k) != v:
|
|
1372
|
+
persons_service.update_person(user["id"], person_info)
|
|
1373
|
+
break
|
|
1374
|
+
except PersonNotFoundException:
|
|
1375
|
+
user = persons_service.create_person(
|
|
1376
|
+
email, "default".encode("utf-8"), **person_info
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
access_token = create_access_token(
|
|
1380
|
+
identity=user["id"],
|
|
1381
|
+
additional_claims={
|
|
1382
|
+
"identity_type": "person",
|
|
1383
|
+
},
|
|
1384
|
+
)
|
|
1385
|
+
refresh_token = create_refresh_token(
|
|
1386
|
+
identity=user["id"],
|
|
1387
|
+
additional_claims={
|
|
1388
|
+
"identity_type": "person",
|
|
1389
|
+
},
|
|
1390
|
+
)
|
|
1391
|
+
identity_changed.send(
|
|
1392
|
+
current_app._get_current_object(),
|
|
1393
|
+
identity=Identity(user["id"], "person"),
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
ip_address = request.environ.get("HTTP_X_REAL_IP", request.remote_addr)
|
|
1397
|
+
|
|
1398
|
+
response = make_response(
|
|
1399
|
+
redirect(f"{config.DOMAIN_PROTOCOL}://{config.DOMAIN_NAME}")
|
|
1400
|
+
)
|
|
1401
|
+
set_access_cookies(response, access_token)
|
|
1402
|
+
set_refresh_cookies(response, refresh_token)
|
|
1403
|
+
events_service.create_login_log(user["id"], ip_address, "web")
|
|
1404
|
+
|
|
1405
|
+
return response
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
class SAMLLoginResource(Resource, ArgsMixin):
|
|
1409
|
+
"""
|
|
1410
|
+
Resource to allow a user to login with SAML SSO.
|
|
1411
|
+
"""
|
|
1412
|
+
|
|
1413
|
+
def get(self):
|
|
1414
|
+
"""
|
|
1415
|
+
Resource to allow a user to login with SAML SSO.
|
|
1416
|
+
---
|
|
1417
|
+
description: ""
|
|
1418
|
+
tags:
|
|
1419
|
+
- Authentication
|
|
1420
|
+
responses:
|
|
1421
|
+
302:
|
|
1422
|
+
description: Redirect to the SAML IDP.
|
|
1423
|
+
400:
|
|
1424
|
+
description: Wrong parameter.
|
|
1425
|
+
"""
|
|
1426
|
+
if not config.SAML_ENABLED:
|
|
1427
|
+
return {"error": "SAML is not enabled."}, 400
|
|
1428
|
+
_, info = current_app.extensions[
|
|
1429
|
+
"saml_client"
|
|
1430
|
+
].prepare_for_authenticate()
|
|
1431
|
+
|
|
1432
|
+
redirect_url = None
|
|
1433
|
+
|
|
1434
|
+
# Select the IdP URL to send the AuthN request to
|
|
1435
|
+
for key, value in info["headers"]:
|
|
1436
|
+
if key == "Location":
|
|
1437
|
+
redirect_url = value
|
|
1438
|
+
|
|
1439
|
+
return redirect(redirect_url, code=302)
|
|
@@ -10,8 +10,11 @@ from zou.app.models.entity import (
|
|
|
10
10
|
EntityVersion,
|
|
11
11
|
EntityLink,
|
|
12
12
|
EntityConceptLink,
|
|
13
|
+
ENTITY_STATUSES,
|
|
13
14
|
)
|
|
15
|
+
from zou.app.models.project import Project
|
|
14
16
|
from zou.app.models.subscription import Subscription
|
|
17
|
+
from zou.app.models.task import Task
|
|
15
18
|
from zou.app.services import (
|
|
16
19
|
assets_service,
|
|
17
20
|
breakdown_service,
|
|
@@ -23,7 +26,9 @@ from zou.app.services import (
|
|
|
23
26
|
user_service,
|
|
24
27
|
concepts_service,
|
|
25
28
|
)
|
|
26
|
-
from zou.app.utils import events,
|
|
29
|
+
from zou.app.utils import date_helpers, events, permissions
|
|
30
|
+
|
|
31
|
+
from zou.app.services.exception import ArgumentsException
|
|
27
32
|
|
|
28
33
|
from werkzeug.exceptions import NotFound
|
|
29
34
|
|
|
@@ -56,11 +61,36 @@ class EntitiesResource(BaseModelsResource, EntityEventMixin):
|
|
|
56
61
|
def emit_create_event(self, entity_dict):
|
|
57
62
|
self.emit_event("new", entity_dict)
|
|
58
63
|
|
|
64
|
+
def check_read_permissions(self):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
def add_project_permission_filter(self, query):
|
|
68
|
+
if not permissions.has_admin_permissions():
|
|
69
|
+
query = query.join(Project).filter(
|
|
70
|
+
user_service.build_related_projects_filter()
|
|
71
|
+
)
|
|
72
|
+
if permissions.has_vendor_permissions():
|
|
73
|
+
query = query.join(Task).filter(
|
|
74
|
+
user_service.build_assignee_filter()
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return query
|
|
78
|
+
|
|
59
79
|
def update_data(self, data):
|
|
60
80
|
data = super().update_data(data)
|
|
61
81
|
data["created_by"] = persons_service.get_current_user()["id"]
|
|
62
82
|
return data
|
|
63
83
|
|
|
84
|
+
def check_creation_integrity(self, data):
|
|
85
|
+
"""
|
|
86
|
+
Check if entity has a valid status.
|
|
87
|
+
"""
|
|
88
|
+
if "status" in data:
|
|
89
|
+
types = [entity_status for entity_status, _ in ENTITY_STATUSES]
|
|
90
|
+
if data["status"] not in types:
|
|
91
|
+
raise ArgumentsException("Invalid status")
|
|
92
|
+
return True
|
|
93
|
+
|
|
64
94
|
def all_entries(self, query=None, relations=False):
|
|
65
95
|
entities = BaseModelsResource.all_entries(
|
|
66
96
|
self, query=query, relations=relations
|
|
@@ -216,3 +246,14 @@ class EntityResource(BaseModelResource, EntityEventMixin):
|
|
|
216
246
|
index_service.remove_asset_index(entity_dict["id"])
|
|
217
247
|
elif shots_service.is_shot(entity_dict):
|
|
218
248
|
index_service.remove_shot_index(entity_dict["id"])
|
|
249
|
+
|
|
250
|
+
def update_data(self, data, instance_id):
|
|
251
|
+
"""
|
|
252
|
+
Check if the entity has a valid status.
|
|
253
|
+
"""
|
|
254
|
+
data = super().update_data(data, instance_id)
|
|
255
|
+
if "status" in data:
|
|
256
|
+
types = [entity_status for entity_status, _ in ENTITY_STATUSES]
|
|
257
|
+
if data["status"] not in types:
|
|
258
|
+
raise ArgumentsException("Invalid status")
|
|
259
|
+
return data
|
zou/app/blueprints/crud/task.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from flask import request, current_app
|
|
2
2
|
from flask_jwt_extended import jwt_required
|
|
3
3
|
|
|
4
|
+
from sqlalchemy.orm import aliased
|
|
4
5
|
from sqlalchemy.exc import IntegrityError
|
|
6
|
+
|
|
5
7
|
from zou.app.mixin import ArgsMixin
|
|
8
|
+
from zou.app.models.entity import Entity
|
|
6
9
|
from zou.app.models.person import Person
|
|
7
10
|
from zou.app.models.project import Project
|
|
8
11
|
from zou.app.models.task import Task
|
|
@@ -21,7 +24,7 @@ from zou.app.services.exception import WrongTaskTypeForEntityException
|
|
|
21
24
|
from zou.app.blueprints.crud.base import BaseModelsResource, BaseModelResource
|
|
22
25
|
|
|
23
26
|
|
|
24
|
-
class TasksResource(BaseModelsResource):
|
|
27
|
+
class TasksResource(BaseModelsResource, ArgsMixin):
|
|
25
28
|
def __init__(self):
|
|
26
29
|
BaseModelsResource.__init__(self, Task)
|
|
27
30
|
|
|
@@ -37,6 +40,43 @@ class TasksResource(BaseModelsResource):
|
|
|
37
40
|
)
|
|
38
41
|
return query
|
|
39
42
|
|
|
43
|
+
def build_filters(self, options):
|
|
44
|
+
(
|
|
45
|
+
many_join_filter,
|
|
46
|
+
in_filter,
|
|
47
|
+
name_filter,
|
|
48
|
+
criterions,
|
|
49
|
+
) = super().build_filters(options)
|
|
50
|
+
if "project_id" in criterions:
|
|
51
|
+
del criterions["project_id"]
|
|
52
|
+
if "episode_id" in criterions:
|
|
53
|
+
del criterions["episode_id"]
|
|
54
|
+
return (
|
|
55
|
+
many_join_filter,
|
|
56
|
+
in_filter,
|
|
57
|
+
name_filter,
|
|
58
|
+
criterions,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def apply_filters(self, query, options):
|
|
62
|
+
query = super().apply_filters(query, options)
|
|
63
|
+
|
|
64
|
+
project_id = options.get("project_id", None)
|
|
65
|
+
episode_id = options.get("episode_id", None)
|
|
66
|
+
if episode_id is not None:
|
|
67
|
+
Sequence = aliased(Entity)
|
|
68
|
+
query = (
|
|
69
|
+
query.join(Entity, Task.entity_id == Entity.id)
|
|
70
|
+
.join(Sequence, Entity.parent_id == Sequence.id)
|
|
71
|
+
.filter(Sequence.parent_id == episode_id)
|
|
72
|
+
)
|
|
73
|
+
elif project_id is not None:
|
|
74
|
+
query = query.join(Entity, Task.entity_id == Entity.id).filter(
|
|
75
|
+
Entity.project_id == project_id
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return query
|
|
79
|
+
|
|
40
80
|
def post(self):
|
|
41
81
|
"""
|
|
42
82
|
Create a task with data given in the request body. JSON format is
|
|
@@ -25,7 +25,7 @@ class IndexResource(Resource):
|
|
|
25
25
|
200:
|
|
26
26
|
description: API name and version
|
|
27
27
|
"""
|
|
28
|
-
return {"api":
|
|
28
|
+
return {"api": config.APP_NAME, "version": __version__}
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class BaseStatusResource(Resource):
|
|
@@ -86,10 +86,8 @@ class BaseStatusResource(Resource):
|
|
|
86
86
|
|
|
87
87
|
version = __version__
|
|
88
88
|
|
|
89
|
-
api_name = app.config["APP_NAME"]
|
|
90
|
-
|
|
91
89
|
return (
|
|
92
|
-
|
|
90
|
+
config.APP_NAME,
|
|
93
91
|
version,
|
|
94
92
|
is_db_up,
|
|
95
93
|
is_kv_up,
|
|
@@ -281,20 +279,22 @@ class ConfigResource(Resource):
|
|
|
281
279
|
200:
|
|
282
280
|
description: Crisp token
|
|
283
281
|
"""
|
|
284
|
-
|
|
285
|
-
"is_self_hosted":
|
|
286
|
-
"crisp_token":
|
|
282
|
+
conf = {
|
|
283
|
+
"is_self_hosted": config.IS_SELF_HOSTED,
|
|
284
|
+
"crisp_token": config.CRISP_TOKEN,
|
|
287
285
|
"indexer_configured": (
|
|
288
|
-
len(
|
|
289
|
-
and
|
|
286
|
+
len(config.INDEXER["key"]) > 0
|
|
287
|
+
and config.INDEXER["key"] != "masterkey"
|
|
290
288
|
),
|
|
289
|
+
"saml_enabled": config.SAML_ENABLED,
|
|
290
|
+
"saml_idp_name": config.SAML_IDP_NAME,
|
|
291
291
|
}
|
|
292
|
-
if
|
|
293
|
-
|
|
294
|
-
"dsn":
|
|
295
|
-
"sampleRate":
|
|
292
|
+
if config.SENTRY_KITSU_ENABLED:
|
|
293
|
+
conf["sentry"] = {
|
|
294
|
+
"dsn": config.SENTRY_KITSU_DSN,
|
|
295
|
+
"sampleRate": config.SENTRY_KITSU_SR,
|
|
296
296
|
}
|
|
297
|
-
return
|
|
297
|
+
return conf
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
class TestEventsResource(Resource):
|
|
@@ -18,7 +18,10 @@ from zou.app.services import (
|
|
|
18
18
|
|
|
19
19
|
from zou.app.mixin import ArgsMixin
|
|
20
20
|
from zou.app.utils import fields, query, permissions
|
|
21
|
-
from zou.app.services.exception import
|
|
21
|
+
from zou.app.services.exception import (
|
|
22
|
+
WrongParameterException,
|
|
23
|
+
ArgumentsException,
|
|
24
|
+
)
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class ShotResource(Resource, ArgsMixin):
|
|
@@ -48,6 +51,52 @@ class ShotResource(Resource, ArgsMixin):
|
|
|
48
51
|
user_service.check_entity_access(shot["id"])
|
|
49
52
|
return shot
|
|
50
53
|
|
|
54
|
+
@jwt_required()
|
|
55
|
+
def put(self, shot_id):
|
|
56
|
+
"""
|
|
57
|
+
Update given shot.
|
|
58
|
+
---
|
|
59
|
+
tags:
|
|
60
|
+
- Shots
|
|
61
|
+
parameters:
|
|
62
|
+
- in: path
|
|
63
|
+
name: shot_id
|
|
64
|
+
required: True
|
|
65
|
+
type: string
|
|
66
|
+
format: UUID
|
|
67
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
68
|
+
- in: body
|
|
69
|
+
name: data
|
|
70
|
+
required: True
|
|
71
|
+
type: object
|
|
72
|
+
responses:
|
|
73
|
+
200:
|
|
74
|
+
description: Update given shot
|
|
75
|
+
"""
|
|
76
|
+
shot = shots_service.get_shot(shot_id)
|
|
77
|
+
user_service.check_manager_project_access(shot["project_id"])
|
|
78
|
+
data = request.json
|
|
79
|
+
if data is None:
|
|
80
|
+
raise ArgumentsException(
|
|
81
|
+
"Data are empty. Please verify that you sent JSON data and"
|
|
82
|
+
" that you set the right headers."
|
|
83
|
+
)
|
|
84
|
+
for field in [
|
|
85
|
+
"id",
|
|
86
|
+
"created_at",
|
|
87
|
+
"updated_at",
|
|
88
|
+
"instance_casting",
|
|
89
|
+
"project_id",
|
|
90
|
+
"entities_in",
|
|
91
|
+
"entities_out",
|
|
92
|
+
"type",
|
|
93
|
+
"shotgun_id",
|
|
94
|
+
"created_by",
|
|
95
|
+
]:
|
|
96
|
+
data.pop(field, None)
|
|
97
|
+
|
|
98
|
+
return shots_service.update_shot(shot_id, data)
|
|
99
|
+
|
|
51
100
|
@jwt_required()
|
|
52
101
|
def delete(self, shot_id):
|
|
53
102
|
"""
|
|
@@ -1463,7 +1463,6 @@ class ProjectTasksResource(Resource, ArgsMixin):
|
|
|
1463
1463
|
"""
|
|
1464
1464
|
|
|
1465
1465
|
@jwt_required()
|
|
1466
|
-
@permissions.require_admin
|
|
1467
1466
|
def get(self, project_id):
|
|
1468
1467
|
"""
|
|
1469
1468
|
Retrieve all tasks related to given project.
|
|
@@ -1478,13 +1477,34 @@ class ProjectTasksResource(Resource, ArgsMixin):
|
|
|
1478
1477
|
type: string
|
|
1479
1478
|
format: UUID
|
|
1480
1479
|
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
1480
|
+
- in: query
|
|
1481
|
+
name: page
|
|
1482
|
+
required: False
|
|
1483
|
+
type: integer
|
|
1484
|
+
x-example: 1
|
|
1485
|
+
- in: query
|
|
1486
|
+
name: task_type_id
|
|
1487
|
+
required: False
|
|
1488
|
+
type: string
|
|
1489
|
+
format: UUID
|
|
1490
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
1491
|
+
- in: query
|
|
1492
|
+
name: episode_id
|
|
1493
|
+
required: False
|
|
1494
|
+
type: string
|
|
1495
|
+
format: UUID
|
|
1496
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
1481
1497
|
responses:
|
|
1482
1498
|
200:
|
|
1483
1499
|
description: All tasks related to given project
|
|
1484
1500
|
"""
|
|
1485
1501
|
projects_service.get_project(project_id)
|
|
1486
1502
|
page = self.get_page()
|
|
1487
|
-
|
|
1503
|
+
task_type_id = self.get_task_type_id()
|
|
1504
|
+
episode_id = self.get_episode_id()
|
|
1505
|
+
return tasks_service.get_tasks_for_project(
|
|
1506
|
+
project_id, page, task_type_id=task_type_id, episode_id=episode_id
|
|
1507
|
+
)
|
|
1488
1508
|
|
|
1489
1509
|
|
|
1490
1510
|
class ProjectCommentsResource(Resource, ArgsMixin):
|
zou/app/config.py
CHANGED
|
@@ -132,6 +132,10 @@ LDAP_IS_AD = envtobool("LDAP_IS_AD", False)
|
|
|
132
132
|
LDAP_IS_AD_SIMPLE = envtobool("LDAP_IS_AD_SIMPLE", False)
|
|
133
133
|
LDAP_SSL = envtobool("LDAP_SSL", False)
|
|
134
134
|
|
|
135
|
+
SAML_ENABLED = envtobool("SAML_ENABLED", False)
|
|
136
|
+
SAML_IDP_NAME = os.getenv("SAML_IDP_NAME", "")
|
|
137
|
+
SAML_METADATA_URL = os.getenv("SAML_METADATA_URL", "")
|
|
138
|
+
|
|
135
139
|
LOGS_MODE = os.getenv("LOGS_MODE", "default")
|
|
136
140
|
LOGS_HOST = os.getenv("LOGS_HOST", "localhost")
|
|
137
141
|
LOGS_PORT = os.getenv("LOGS_PORT", 2202)
|
zou/app/services/auth_service.py
CHANGED
|
@@ -2,7 +2,6 @@ import pyotp
|
|
|
2
2
|
import random
|
|
3
3
|
import string
|
|
4
4
|
import flask_bcrypt
|
|
5
|
-
import fido2.features
|
|
6
5
|
|
|
7
6
|
from datetime import timedelta
|
|
8
7
|
|
|
@@ -37,29 +36,15 @@ from zou.app.services.exception import (
|
|
|
37
36
|
)
|
|
38
37
|
from zou.app.stores import auth_tokens_store
|
|
39
38
|
from zou.app.utils import date_helpers, emails
|
|
40
|
-
from zou.app import config
|
|
41
39
|
|
|
42
40
|
from fido2.webauthn import (
|
|
43
|
-
PublicKeyCredentialRpEntity,
|
|
44
41
|
PublicKeyCredentialUserEntity,
|
|
45
42
|
)
|
|
46
|
-
from fido2.server import Fido2Server
|
|
47
43
|
from sqlalchemy.orm.attributes import flag_modified
|
|
48
|
-
|
|
44
|
+
|
|
49
45
|
from fido2.utils import bytes2int, int2bytes
|
|
50
46
|
from fido2.webauthn import AttestedCredentialData
|
|
51
47
|
|
|
52
|
-
fido2.features.webauthn_json_mapping.enabled = True
|
|
53
|
-
|
|
54
|
-
fido_server = Fido2Server(
|
|
55
|
-
PublicKeyCredentialRpEntity(
|
|
56
|
-
name="Kitsu", id=urlparse(f"https://{config.DOMAIN_NAME}").hostname
|
|
57
|
-
),
|
|
58
|
-
verify_origin=(
|
|
59
|
-
None if config.DOMAIN_NAME != "localhost:8080" else lambda a: True
|
|
60
|
-
),
|
|
61
|
-
)
|
|
62
|
-
|
|
63
48
|
|
|
64
49
|
def check_auth(
|
|
65
50
|
app,
|
|
@@ -345,7 +330,7 @@ def check_fido(person, authentication_response):
|
|
|
345
330
|
except KeyError:
|
|
346
331
|
return False
|
|
347
332
|
try:
|
|
348
|
-
fido_server.authenticate_complete(
|
|
333
|
+
current_app.extensions["fido_server"].authenticate_complete(
|
|
349
334
|
state,
|
|
350
335
|
get_fido_attested_credential_data_from_person(
|
|
351
336
|
person["fido_credentials"],
|
|
@@ -551,7 +536,7 @@ def pre_register_fido(person_id):
|
|
|
551
536
|
Pre-register FIDO device for a person.
|
|
552
537
|
"""
|
|
553
538
|
person = Person.get(person_id)
|
|
554
|
-
options, state = fido_server.register_begin(
|
|
539
|
+
options, state = current_app.extensions["fido_server"].register_begin(
|
|
555
540
|
PublicKeyCredentialUserEntity(
|
|
556
541
|
id=str(person.id).encode(),
|
|
557
542
|
name=person.email,
|
|
@@ -577,7 +562,9 @@ def register_fido(person_id, registration_response, device_name):
|
|
|
577
562
|
except KeyError:
|
|
578
563
|
raise FIDONoPreregistrationException()
|
|
579
564
|
try:
|
|
580
|
-
auth_data = fido_server.register_complete(
|
|
565
|
+
auth_data = current_app.extensions["fido_server"].register_complete(
|
|
566
|
+
state, registration_response
|
|
567
|
+
)
|
|
581
568
|
except BaseException:
|
|
582
569
|
raise FIDOServerException()
|
|
583
570
|
credential_data = {
|
|
@@ -638,7 +625,7 @@ def get_challenge_fido(person_id):
|
|
|
638
625
|
Get new FIDO challenge for a person.
|
|
639
626
|
"""
|
|
640
627
|
person = Person.get(person_id)
|
|
641
|
-
options, state = fido_server.authenticate_begin(
|
|
628
|
+
options, state = current_app.extensions["fido_server"].authenticate_begin(
|
|
642
629
|
credentials=get_fido_attested_credential_data_from_person(
|
|
643
630
|
person.fido_credentials
|
|
644
631
|
),
|
|
@@ -276,12 +276,15 @@ def update_password(email, password):
|
|
|
276
276
|
return person.serialize()
|
|
277
277
|
|
|
278
278
|
|
|
279
|
-
def update_person(person_id, data):
|
|
279
|
+
def update_person(person_id, data, bypass_protected_accounts=False):
|
|
280
280
|
"""
|
|
281
281
|
Update person entry with data given in parameter.
|
|
282
282
|
"""
|
|
283
283
|
person = Person.get(person_id)
|
|
284
|
-
if
|
|
284
|
+
if (
|
|
285
|
+
not bypass_protected_accounts
|
|
286
|
+
and person.email in config.PROTECTED_ACCOUNTS
|
|
287
|
+
):
|
|
285
288
|
message = None
|
|
286
289
|
if data.get("active") is False:
|
|
287
290
|
message = (
|
|
@@ -1698,13 +1698,33 @@ def get_time_spents_for_project(project_id, page=0):
|
|
|
1698
1698
|
return query_utils.get_paginated_results(query, page)
|
|
1699
1699
|
|
|
1700
1700
|
|
|
1701
|
-
def get_tasks_for_project(
|
|
1701
|
+
def get_tasks_for_project(
|
|
1702
|
+
project_id, page=0, task_type_id=None, episode_id=None
|
|
1703
|
+
):
|
|
1702
1704
|
"""
|
|
1703
1705
|
Return all tasks for given project.
|
|
1704
1706
|
"""
|
|
1705
1707
|
query = Task.query.filter(Task.project_id == project_id).order_by(
|
|
1706
1708
|
Task.updated_at.desc()
|
|
1707
1709
|
)
|
|
1710
|
+
if task_type_id is not None:
|
|
1711
|
+
query = query.filter(Task.task_type_id == task_type_id)
|
|
1712
|
+
if episode_id is not None:
|
|
1713
|
+
Sequence = aliased(Entity, name="sequence")
|
|
1714
|
+
query = (
|
|
1715
|
+
query.join(Entity, Entity.id == Task.entity_id)
|
|
1716
|
+
.join(Sequence, Sequence.id == Entity.parent_id)
|
|
1717
|
+
.filter(Sequence.parent_id == episode_id)
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
if permissions.has_vendor_permissions():
|
|
1721
|
+
query = query.filter(user_service.build_assignee_filter())
|
|
1722
|
+
elif not permissions.has_admin_permissions():
|
|
1723
|
+
query = query.join(Project).filter(
|
|
1724
|
+
user_service.build_related_projects_filter()
|
|
1725
|
+
)
|
|
1726
|
+
return query
|
|
1727
|
+
|
|
1708
1728
|
return query_utils.get_paginated_results(query, page, relations=True)
|
|
1709
1729
|
|
|
1710
1730
|
|
zou/app/utils/dbhelpers.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from sqlalchemy import create_engine
|
|
1
|
+
from sqlalchemy import create_engine, inspect
|
|
2
2
|
from sqlalchemy_utils import database_exists, create_database
|
|
3
3
|
from sqlalchemy.engine.url import URL
|
|
4
4
|
from sqlalchemy.orm import close_all_sessions
|
|
@@ -39,3 +39,16 @@ def drop_all():
|
|
|
39
39
|
db.session.flush()
|
|
40
40
|
close_all_sessions()
|
|
41
41
|
return db.drop_all()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_init():
|
|
45
|
+
"""
|
|
46
|
+
Check if database is initialized.
|
|
47
|
+
"""
|
|
48
|
+
from zou.app import db
|
|
49
|
+
from zou.app.models.project_status import ProjectStatus
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
inspect(db.engine).has_table("person")
|
|
53
|
+
and db.session.query(ProjectStatus).count() == 2
|
|
54
|
+
)
|
zou/app/utils/fido.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fido2.features
|
|
2
|
+
|
|
3
|
+
from fido2.webauthn import (
|
|
4
|
+
PublicKeyCredentialRpEntity,
|
|
5
|
+
)
|
|
6
|
+
from fido2.server import Fido2Server
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
from zou.app import config
|
|
10
|
+
|
|
11
|
+
fido2.features.webauthn_json_mapping.enabled = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_fido_server():
|
|
15
|
+
return Fido2Server(
|
|
16
|
+
PublicKeyCredentialRpEntity(
|
|
17
|
+
name="Kitsu", id=urlparse(f"https://{config.DOMAIN_NAME}").hostname
|
|
18
|
+
),
|
|
19
|
+
verify_origin=(
|
|
20
|
+
None if config.DOMAIN_NAME != "localhost:8080" else lambda a: True
|
|
21
|
+
),
|
|
22
|
+
)
|
zou/app/utils/saml.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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 = f"http://{config.DOMAIN_NAME}/api/auth/saml/sso"
|
|
19
|
+
https_acs_url = f"https://{config.DOMAIN_NAME}/api/auth/saml/sso"
|
|
20
|
+
|
|
21
|
+
rv = requests.get(metadata_url)
|
|
22
|
+
|
|
23
|
+
settings = {
|
|
24
|
+
"entityid": f"{config.DOMAIN_PROTOCOL}://{config.DOMAIN_NAME}/api/auth/saml/login",
|
|
25
|
+
"metadata": {"inline": [rv.text]},
|
|
26
|
+
"service": {
|
|
27
|
+
"sp": {
|
|
28
|
+
"endpoints": {
|
|
29
|
+
"assertion_consumer_service": [
|
|
30
|
+
(acs_url, BINDING_HTTP_REDIRECT),
|
|
31
|
+
(acs_url, BINDING_HTTP_POST),
|
|
32
|
+
(https_acs_url, BINDING_HTTP_REDIRECT),
|
|
33
|
+
(https_acs_url, BINDING_HTTP_POST),
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
# Don't verify that the incoming requests originate from us via
|
|
37
|
+
# the built-in cache for authn request ids in pysaml2
|
|
38
|
+
"allow_unsolicited": True,
|
|
39
|
+
# Don't sign authn requests, since signed requests only make
|
|
40
|
+
# sense in a situation where you control both the SP and IdP
|
|
41
|
+
"authn_requests_signed": False,
|
|
42
|
+
"logout_requests_signed": True,
|
|
43
|
+
"want_assertions_signed": True,
|
|
44
|
+
"want_response_signed": False,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
spConfig = Saml2Config()
|
|
49
|
+
spConfig.load(settings)
|
|
50
|
+
spConfig.allow_unknown_attributes = True
|
|
51
|
+
saml_client = Saml2Client(config=spConfig)
|
|
52
|
+
return saml_client
|
zou/cli.py
CHANGED
|
@@ -47,6 +47,23 @@ 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 if the database wheter the database is
|
|
54
|
+
initiliazed or not."
|
|
55
|
+
"""
|
|
56
|
+
with app.app_context():
|
|
57
|
+
is_init = dbhelpers.is_init()
|
|
58
|
+
if is_init:
|
|
59
|
+
print("Database is initiliazed.")
|
|
60
|
+
else:
|
|
61
|
+
print(
|
|
62
|
+
"Database is not initiliazed. "
|
|
63
|
+
"Run 'zou init-db' and 'init-data'."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
50
67
|
@cli.command()
|
|
51
68
|
@click.option("--message", default="")
|
|
52
69
|
def migrate_db(message):
|
|
@@ -140,7 +157,7 @@ def reset_migrations():
|
|
|
140
157
|
|
|
141
158
|
@cli.command()
|
|
142
159
|
@click.argument("email")
|
|
143
|
-
@click.option("--password",
|
|
160
|
+
@click.option("--password", default=None)
|
|
144
161
|
def create_admin(email, password):
|
|
145
162
|
"""
|
|
146
163
|
Create an admin user to allow usage of the API when database is empty.
|
|
@@ -149,10 +166,18 @@ def create_admin(email, password):
|
|
|
149
166
|
try:
|
|
150
167
|
person = persons_service.get_person_by_email(email)
|
|
151
168
|
if person["role"] != "admin":
|
|
152
|
-
persons_service.update_person(
|
|
169
|
+
persons_service.update_person(
|
|
170
|
+
person["id"],
|
|
171
|
+
{"role": "admin"},
|
|
172
|
+
bypass_protected_accounts=True,
|
|
173
|
+
)
|
|
153
174
|
print("Existing user's role has been upgraded to 'admin'.")
|
|
154
175
|
except PersonNotFoundException:
|
|
155
176
|
try:
|
|
177
|
+
if password is None:
|
|
178
|
+
raise click.MissingParameter(
|
|
179
|
+
param_type="option", param_hint="--password"
|
|
180
|
+
)
|
|
156
181
|
auth.validate_password(password)
|
|
157
182
|
# Allow "admin@example.com" to be invalid.
|
|
158
183
|
if email != "admin@example.com":
|
|
@@ -8,6 +8,10 @@ Create Date: 2024-07-19 15:24:21.064991
|
|
|
8
8
|
|
|
9
9
|
from alembic import op
|
|
10
10
|
import sqlalchemy as sa
|
|
11
|
+
from sqlalchemy import orm
|
|
12
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
13
|
+
from zou.migrations.utils.base import BaseMixin
|
|
14
|
+
from sqlalchemy_utils import UUIDType, ChoiceType
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
# revision identifiers, used by Alembic.
|
|
@@ -18,6 +22,65 @@ depends_on = None
|
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
def upgrade():
|
|
25
|
+
base = declarative_base()
|
|
26
|
+
|
|
27
|
+
TYPES = [
|
|
28
|
+
("comment", "Comment"),
|
|
29
|
+
("mention", "Mention"),
|
|
30
|
+
("assignation", "Assignation"),
|
|
31
|
+
("reply", "Reply"),
|
|
32
|
+
("reply-mention", "Reply Mention"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
class Notification(base, BaseMixin):
|
|
36
|
+
"""
|
|
37
|
+
A notification is stored each time a comment is posted.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__tablename__ = "notification"
|
|
41
|
+
read = sa.Column(sa.Boolean, nullable=False, default=False)
|
|
42
|
+
change = sa.Column(sa.Boolean, nullable=False, default=False)
|
|
43
|
+
type = sa.Column(ChoiceType(TYPES), nullable=False)
|
|
44
|
+
person_id = sa.Column(
|
|
45
|
+
UUIDType(binary=False),
|
|
46
|
+
nullable=False,
|
|
47
|
+
index=True,
|
|
48
|
+
)
|
|
49
|
+
author_id = sa.Column(
|
|
50
|
+
UUIDType(binary=False),
|
|
51
|
+
nullable=False,
|
|
52
|
+
index=True,
|
|
53
|
+
)
|
|
54
|
+
comment_id = sa.Column(
|
|
55
|
+
UUIDType(binary=False),
|
|
56
|
+
nullable=True,
|
|
57
|
+
index=True,
|
|
58
|
+
)
|
|
59
|
+
task_id = sa.Column(
|
|
60
|
+
UUIDType(binary=False),
|
|
61
|
+
nullable=False,
|
|
62
|
+
index=True,
|
|
63
|
+
)
|
|
64
|
+
reply_id = sa.Column(UUIDType(binary=False), nullable=True, index=True)
|
|
65
|
+
|
|
66
|
+
__table_args__ = (
|
|
67
|
+
sa.UniqueConstraint(
|
|
68
|
+
"person_id",
|
|
69
|
+
"author_id",
|
|
70
|
+
"comment_id",
|
|
71
|
+
"reply_id",
|
|
72
|
+
"type",
|
|
73
|
+
name="notification_uc",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
bind = op.get_bind()
|
|
78
|
+
session = orm.Session(bind=bind)
|
|
79
|
+
session.query(Notification).where(Notification.type == None).update(
|
|
80
|
+
{Notification.type: "comment"}
|
|
81
|
+
)
|
|
82
|
+
session.commit()
|
|
83
|
+
|
|
21
84
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
85
|
with op.batch_alter_table("notification", schema=None) as batch_op:
|
|
23
86
|
batch_op.alter_column(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: zou
|
|
3
|
-
Version: 0.19.
|
|
3
|
+
Version: 0.19.51
|
|
4
4
|
Summary: API to store and manage the data of your animation production
|
|
5
5
|
Home-page: https://zou.cg-wire.com
|
|
6
6
|
Author: CG Wire
|
|
@@ -11,7 +11,6 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
11
11
|
Classifier: Environment :: Web Environment
|
|
12
12
|
Classifier: Framework :: Flask
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -19,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
18
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
20
|
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.9, <3.13
|
|
23
22
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: babel ==2.
|
|
23
|
+
Requires-Dist: babel ==2.16.0
|
|
25
24
|
Requires-Dist: click ==8.1.7
|
|
26
25
|
Requires-Dist: discord.py ==2.4.0
|
|
27
26
|
Requires-Dist: email-validator ==2.2.0
|
|
@@ -40,53 +39,54 @@ Requires-Dist: flask-jwt-extended ==4.6.0
|
|
|
40
39
|
Requires-Dist: flask-migrate ==4.0.7
|
|
41
40
|
Requires-Dist: flask-socketio ==5.3.6
|
|
42
41
|
Requires-Dist: flask ==3.0.3
|
|
43
|
-
Requires-Dist: gazu ==0.10.
|
|
42
|
+
Requires-Dist: gazu ==0.10.14
|
|
44
43
|
Requires-Dist: gevent-websocket ==0.10.1
|
|
45
44
|
Requires-Dist: gevent ==24.2.1
|
|
46
|
-
Requires-Dist: gunicorn ==
|
|
45
|
+
Requires-Dist: gunicorn ==23.0.0
|
|
47
46
|
Requires-Dist: isoweek ==1.3.3
|
|
48
47
|
Requires-Dist: itsdangerous ==2.2.0
|
|
49
48
|
Requires-Dist: Jinja2 ==3.1.4
|
|
50
49
|
Requires-Dist: ldap3 ==2.9.1
|
|
51
50
|
Requires-Dist: matterhook ==0.2
|
|
52
|
-
Requires-Dist: meilisearch ==0.31.
|
|
51
|
+
Requires-Dist: meilisearch ==0.31.5
|
|
53
52
|
Requires-Dist: opencv-python ==4.10.0.84
|
|
54
53
|
Requires-Dist: OpenTimelineIO ==0.17.0
|
|
55
54
|
Requires-Dist: OpenTimelineIO-Plugins ==0.17.0
|
|
56
|
-
Requires-Dist: orjson ==3.10.
|
|
55
|
+
Requires-Dist: orjson ==3.10.7
|
|
57
56
|
Requires-Dist: pillow ==10.4.0
|
|
58
57
|
Requires-Dist: psutil ==6.0.0
|
|
59
58
|
Requires-Dist: psycopg[binary] ==3.2.1
|
|
60
59
|
Requires-Dist: pyotp ==2.9.0
|
|
60
|
+
Requires-Dist: pysaml2 ==7.5.0
|
|
61
61
|
Requires-Dist: python-nomad ==2.0.1
|
|
62
62
|
Requires-Dist: python-slugify ==8.0.4
|
|
63
63
|
Requires-Dist: python-socketio ==5.11.3
|
|
64
64
|
Requires-Dist: pytz ==2024.1
|
|
65
|
-
Requires-Dist: redis ==5.0.
|
|
65
|
+
Requires-Dist: redis ==5.0.8
|
|
66
66
|
Requires-Dist: requests ==2.32.3
|
|
67
67
|
Requires-Dist: rq ==1.16.2
|
|
68
68
|
Requires-Dist: slackclient ==2.9.4
|
|
69
69
|
Requires-Dist: sqlalchemy-utils ==0.41.2
|
|
70
|
-
Requires-Dist: sqlalchemy ==2.0.
|
|
70
|
+
Requires-Dist: sqlalchemy ==2.0.32
|
|
71
71
|
Requires-Dist: ua-parser ==0.18.0
|
|
72
|
-
Requires-Dist: werkzeug ==3.0.
|
|
73
|
-
Requires-Dist: numpy ==
|
|
74
|
-
Requires-Dist: numpy ==2.0
|
|
72
|
+
Requires-Dist: werkzeug ==3.0.4
|
|
73
|
+
Requires-Dist: numpy ==2.0.1 ; python_version == "3.9"
|
|
74
|
+
Requires-Dist: numpy ==2.1.0 ; python_version >= "3.10"
|
|
75
75
|
Provides-Extra: dev
|
|
76
76
|
Requires-Dist: wheel ; extra == 'dev'
|
|
77
77
|
Provides-Extra: lint
|
|
78
78
|
Requires-Dist: autoflake ==2.3.1 ; extra == 'lint'
|
|
79
|
-
Requires-Dist: black ==24.
|
|
79
|
+
Requires-Dist: black ==24.8.0 ; extra == 'lint'
|
|
80
80
|
Requires-Dist: pre-commit ==3.8.0 ; (python_version >= "3.9") and extra == 'lint'
|
|
81
81
|
Provides-Extra: monitoring
|
|
82
82
|
Requires-Dist: prometheus-flask-exporter ==0.23.1 ; extra == 'monitoring'
|
|
83
83
|
Requires-Dist: pygelf ==0.4.2 ; extra == 'monitoring'
|
|
84
|
-
Requires-Dist: sentry-sdk ==2.
|
|
84
|
+
Requires-Dist: sentry-sdk ==2.13.0 ; extra == 'monitoring'
|
|
85
85
|
Provides-Extra: prod
|
|
86
86
|
Requires-Dist: gunicorn ; extra == 'prod'
|
|
87
87
|
Requires-Dist: gevent ; extra == 'prod'
|
|
88
88
|
Provides-Extra: test
|
|
89
|
-
Requires-Dist: fakeredis ==2.
|
|
89
|
+
Requires-Dist: fakeredis ==2.24.1 ; extra == 'test'
|
|
90
90
|
Requires-Dist: mixer ==7.2.2 ; extra == 'test'
|
|
91
91
|
Requires-Dist: pytest-cov ==5.0.0 ; extra == 'test'
|
|
92
92
|
Requires-Dist: pytest ==8.3.2 ; extra == 'test'
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
zou/__init__.py,sha256=
|
|
2
|
-
zou/cli.py,sha256=
|
|
1
|
+
zou/__init__.py,sha256=DP_6242_0xEPjYzU261U5eWC344pjovKgG2asNRwNpU,24
|
|
2
|
+
zou/cli.py,sha256=DHHf4mz33Bi0gGFJ0GdRUpjrMrMSQYyVubJFppHPZlU,18914
|
|
3
3
|
zou/debug.py,sha256=1fawPbkD4wn0Y9Gk0BiBFSa-CQe5agFi8R9uJYl2Uyk,520
|
|
4
4
|
zou/event_stream.py,sha256=zgob2dZKray2lxPa11hdRNmOg8XRlKDdRcGdF80ylwg,8245
|
|
5
5
|
zou/job_settings.py,sha256=WB_RkYxmh4ffQHqg63_wsUDiS0zP3yxiwrxK7DhHG0g,150
|
|
6
|
-
zou/app/__init__.py,sha256=
|
|
6
|
+
zou/app/__init__.py,sha256=1kXv0FUo0XUIoIqZO4268OcLT4Am6pzSYKAcnHrbPW4,6949
|
|
7
7
|
zou/app/api.py,sha256=JTB_IMVO8EOoyqx9KdRkiIix0chOLi0yGDY-verUJXA,5127
|
|
8
|
-
zou/app/config.py,sha256=
|
|
8
|
+
zou/app/config.py,sha256=wA12qGUc7cCVmuD6XNHJ2dJ9mR4-8GnqEFoiS_yJv9s,6544
|
|
9
9
|
zou/app/mixin.py,sha256=eYwfS_CUFvNmldaQXrjsN5mK_gX0wYrBFykfx60uUM8,4897
|
|
10
10
|
zou/app/swagger.py,sha256=UW9DSik3a8GuH1_-7F5P7EYgjZ9DA_kFjukO-6n4kgk,54682
|
|
11
11
|
zou/app/blueprints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
zou/app/blueprints/assets/__init__.py,sha256=tSRvVrnPj732F4k_lkxoTJBJNmIUAqKXPLJDHvOtAA4,2178
|
|
13
13
|
zou/app/blueprints/assets/resources.py,sha256=qY4K890kTTRWUVnNomilEuJY7qBy4RWHG2XiTuWPfBA,18988
|
|
14
|
-
zou/app/blueprints/auth/__init__.py,sha256=
|
|
15
|
-
zou/app/blueprints/auth/resources.py,sha256=
|
|
14
|
+
zou/app/blueprints/auth/__init__.py,sha256=xP874bMWUnLIirOPSEbpe-Q2fBBQrxZGKd0tLZmNJXk,1128
|
|
15
|
+
zou/app/blueprints/auth/resources.py,sha256=xNyMPc9jxAhgPpUdg0Bo3x_8D4lckiLVQchVyRovdI4,44998
|
|
16
16
|
zou/app/blueprints/breakdown/__init__.py,sha256=Dp6GWSGxxWIedpyzTTEKpCRUYEo8oVNVyQhwNvTMmQM,1888
|
|
17
17
|
zou/app/blueprints/breakdown/resources.py,sha256=JY3j17kv7xPKV9S-XoOxNYivpGHlfZkXI5WuGtx3_Yk,13591
|
|
18
18
|
zou/app/blueprints/chats/__init__.py,sha256=YGmwGvddg3MgSYVIh-hmkX8t2em9_LblxBeJzFqFJD4,558
|
|
@@ -31,7 +31,7 @@ zou/app/blueprints/crud/comments.py,sha256=YH4cllfGloPMcIq1tgpT5pSvKTwNJ1zpkUssr
|
|
|
31
31
|
zou/app/blueprints/crud/custom_action.py,sha256=Nf6S6YBxePg4-nfaPTroIYZGWt9fJEHC719DOKJ8R98,978
|
|
32
32
|
zou/app/blueprints/crud/day_off.py,sha256=y0vahwLa7F9u6K5KUQ_6vt7XxHWQ_9vBdzDxvEMGIKg,2496
|
|
33
33
|
zou/app/blueprints/crud/department.py,sha256=AAN_qxZDQGS-mR1kRS_2qGx1D7PKT3h_Ke3IXtJ_F90,973
|
|
34
|
-
zou/app/blueprints/crud/entity.py,sha256=
|
|
34
|
+
zou/app/blueprints/crud/entity.py,sha256=8ZVzvdFWkA_gCZ_88aPyDJyhKyfzRqRyJGjtM5gRs6c,9716
|
|
35
35
|
zou/app/blueprints/crud/entity_link.py,sha256=PlHGy3RPDUVdwWtOpQegz-wZCcxe9FZw_80nUVBy1Ac,819
|
|
36
36
|
zou/app/blueprints/crud/entity_type.py,sha256=eiOwyoxwA2r857vX6Nni1ukXnNnV-6GnpjIGuTrRqrI,1927
|
|
37
37
|
zou/app/blueprints/crud/event.py,sha256=bay2ggg5Oa69lOxNYGkHVQdSKuMxGhk2wqWP7QApKUg,589
|
|
@@ -56,7 +56,7 @@ zou/app/blueprints/crud/software.py,sha256=8UmSPMX-V210nlX0k_RafV930tYqPayt2mBOy
|
|
|
56
56
|
zou/app/blueprints/crud/status_automation.py,sha256=VYgqYlPDmXG38qvACY2UXZNVVqiWjjZM0jD2_2746Kw,2347
|
|
57
57
|
zou/app/blueprints/crud/studio.py,sha256=GviLxRFt8SDE9F58MoVLdXLE1DXq9IlaZB23RrQiz5g,937
|
|
58
58
|
zou/app/blueprints/crud/subscription.py,sha256=xFV_nvRg2ow6E0JWo4VHgeK_SkKX3K8i80thY3qRCl0,392
|
|
59
|
-
zou/app/blueprints/crud/task.py,sha256=
|
|
59
|
+
zou/app/blueprints/crud/task.py,sha256=a-BJ5mgE3dRDUiE-KaZjho7vgJ2qbm9D6d36ZWhWrZA,5672
|
|
60
60
|
zou/app/blueprints/crud/task_status.py,sha256=SAWI7qG_9QsBZY5CpjZABYE0zuCimVzhfZfyEfQ_Rj8,1468
|
|
61
61
|
zou/app/blueprints/crud/task_type.py,sha256=5DLFkIBl8EmCBBTTD888juRFzaYa2wPrwJTdtGVHtHE,1786
|
|
62
62
|
zou/app/blueprints/crud/time_spent.py,sha256=9tfoKJ77OEDTnpKHshO3UMDz_KCoyLq1CWquInm5DY0,3057
|
|
@@ -83,7 +83,7 @@ zou/app/blueprints/export/csv/time_spents.py,sha256=WHdy9Wf2e-kvviYbNyUFr7xgg49h
|
|
|
83
83
|
zou/app/blueprints/files/__init__.py,sha256=7Wty30JW2OXIn-tBFXOWWmPuHnsnxPpH3jNtHvvr9tY,3987
|
|
84
84
|
zou/app/blueprints/files/resources.py,sha256=8SIV8kaqv3dxyL8nyqG3QiZmk5ZYIvUxw6k1ic-jhBs,69786
|
|
85
85
|
zou/app/blueprints/index/__init__.py,sha256=Dh3oQiirpg8RCkfVOuk3irIjSvUvuRf0jPxE6oGubz0,828
|
|
86
|
-
zou/app/blueprints/index/resources.py,sha256=
|
|
86
|
+
zou/app/blueprints/index/resources.py,sha256=N0u0RWxThnRndfb5dMy4o2E45ePc3jxWRDw1e3uZqas,8167
|
|
87
87
|
zou/app/blueprints/news/__init__.py,sha256=HxBXjC15dVbotNAZ0CLf02iwUjxJr20kgf8_kT_9nwM,505
|
|
88
88
|
zou/app/blueprints/news/resources.py,sha256=6e3Ex_Q-djxWDjQQ-eD1fBcylea6XaJphO7Tl9zWKcY,7057
|
|
89
89
|
zou/app/blueprints/persons/__init__.py,sha256=0cnHHw3K_8OEMm0qOi3wKVomSAg9IJSnVjAXabMeHks,3893
|
|
@@ -91,13 +91,13 @@ zou/app/blueprints/persons/resources.py,sha256=wYM5xcjP7Q0-ETZTom4yjvOnTO9kYzfjY
|
|
|
91
91
|
zou/app/blueprints/playlists/__init__.py,sha256=vuEk1F3hFHsmuKWhdepMoLyOzmNKDn1YrjjfcaIz0lQ,1596
|
|
92
92
|
zou/app/blueprints/playlists/resources.py,sha256=alRlMHypUFErXLsEYxpFK84cdjFJ3YWwamZtW0KcwLY,17211
|
|
93
93
|
zou/app/blueprints/previews/__init__.py,sha256=qGohO6LRNZKXBAegINcUXuZlrtxobJKQg84-rQ1L3AU,4202
|
|
94
|
-
zou/app/blueprints/previews/resources.py,sha256=
|
|
94
|
+
zou/app/blueprints/previews/resources.py,sha256=KIiGcnrUfCjpiCGg_Bt3b8URMmuUrjcX9QNN7-Sx1iU,47074
|
|
95
95
|
zou/app/blueprints/projects/__init__.py,sha256=Pn3fA5bpNFEPBzxTKJ2foV6osZFflXXSM2l2uZh3ktM,3927
|
|
96
96
|
zou/app/blueprints/projects/resources.py,sha256=v9_TLh3mujL-p7QcGkfSOJnNojzoJA15jhqAHz5kEIc,31552
|
|
97
97
|
zou/app/blueprints/search/__init__.py,sha256=QCjQIY_85l_orhdEiqav_GifjReuwsjZggN3V0GeUVY,356
|
|
98
98
|
zou/app/blueprints/search/resources.py,sha256=ni-dX8Xfib_0FonLttoXgntXBR957-xhifPSQHHnOnY,2696
|
|
99
99
|
zou/app/blueprints/shots/__init__.py,sha256=HfgLneZBYUMa2OGwIgEZTz8zrIEYFRiYmRbreBPYeYw,4076
|
|
100
|
-
zou/app/blueprints/shots/resources.py,sha256=
|
|
100
|
+
zou/app/blueprints/shots/resources.py,sha256=Dl2uFAcYVrvSPLBv5xt7i0z0MvawMLMHUl1dDm9mJ7c,49955
|
|
101
101
|
zou/app/blueprints/source/__init__.py,sha256=H7K-4TDs4pc5EJvcYTYMJBHesxyqsE5-xq7J8ckOS2g,6093
|
|
102
102
|
zou/app/blueprints/source/kitsu.py,sha256=4lWdqxaKDzwx-5POAIHIgZ6ODbDMOOVRxaSb_FOLcCk,5012
|
|
103
103
|
zou/app/blueprints/source/otio.py,sha256=WkzpKylVkNlbY_jwf6uV5-HPylrktQznOcbCs_p8TDQ,13391
|
|
@@ -127,7 +127,7 @@ zou/app/blueprints/source/shotgun/tasks.py,sha256=XXBRe9QhhS-kuZeV3HitOnpf7mmWVx
|
|
|
127
127
|
zou/app/blueprints/source/shotgun/team.py,sha256=GF7y2BwDeFJCiidtG68icfCi-uV1-b96YKiH8KR54iE,1819
|
|
128
128
|
zou/app/blueprints/source/shotgun/versions.py,sha256=8Mb35e5p3FLbbiu6AZb9tJErDKz2pPRBdIYu80Ayj7w,2292
|
|
129
129
|
zou/app/blueprints/tasks/__init__.py,sha256=pNUqVEVX1KVu1IzRRJNsziPkhWKXqgyvQsNp7LbmIxo,4342
|
|
130
|
-
zou/app/blueprints/tasks/resources.py,sha256=
|
|
130
|
+
zou/app/blueprints/tasks/resources.py,sha256=RTMvVbVC-BBnGnIVuHioaMqMKf2PwgZMXqydOC_axus,55923
|
|
131
131
|
zou/app/blueprints/user/__init__.py,sha256=dhNMmwLvlFqehC7-DPyjCNNkru1gmhPu4WEopHwIabI,4433
|
|
132
132
|
zou/app/blueprints/user/resources.py,sha256=YoPoPt0G4qZhbZFiBCbZ7e3nT3Xf6jgN8FITHPDV2P0,37305
|
|
133
133
|
zou/app/file_trees/default.json,sha256=ryUrEmQYE8B_WkzCoQLgmem3N9yNwMIWx9G8p3HfG9o,2310
|
|
@@ -180,7 +180,7 @@ zou/app/models/time_spent.py,sha256=n7i3FO9g1eE_zATkItoCgrGVqq3iMSfdlKSveEZPloc,
|
|
|
180
180
|
zou/app/models/working_file.py,sha256=q0LM3s1ziw_9AmmPDCkwyf1-TJkWTBMgo2LdHyVRwxg,1509
|
|
181
181
|
zou/app/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
182
182
|
zou/app/services/assets_service.py,sha256=BvAi5mnvZvMJJXnxFF3bofu4JZjiPvdlPrgUyW8PuKY,21460
|
|
183
|
-
zou/app/services/auth_service.py,sha256=
|
|
183
|
+
zou/app/services/auth_service.py,sha256=hQpNb21xlr5EiTrXnzpFb4W4GDtglubqA2z_-YIBfnk,22897
|
|
184
184
|
zou/app/services/backup_service.py,sha256=_ZtZp6wkcVYnHxBosziwLGdrTvsUttXGphiydq53iy8,4840
|
|
185
185
|
zou/app/services/base_service.py,sha256=OZd0STFh-DyBBdwsmA7DMMnrwv4C8wJUbShvZ1isndU,1383
|
|
186
186
|
zou/app/services/breakdown_service.py,sha256=p93HncjC36qbmvZiB259QGRtciS37N-s_7IWVAQx5eE,26727
|
|
@@ -200,7 +200,7 @@ zou/app/services/index_service.py,sha256=1w8rGJ7ArYC13B43hD4JOPtVhaRsbseJVVY-GnU
|
|
|
200
200
|
zou/app/services/names_service.py,sha256=IvwQxlvFpcreVQ1bpy20uIlzEzS8PJayx-sMFuOBots,2917
|
|
201
201
|
zou/app/services/news_service.py,sha256=ltMVObZiNpSat4qze4z6W5zoy9IdPXiIfsqSr2awdWk,8767
|
|
202
202
|
zou/app/services/notifications_service.py,sha256=7GDRio_mGaRYV5BHOAdpxBZjA_LLYUfVpbwZqy1n9pI,15685
|
|
203
|
-
zou/app/services/persons_service.py,sha256=
|
|
203
|
+
zou/app/services/persons_service.py,sha256=06CyF--QnGVkjvZPkAYIBz8V-P_Nbb4Iv1lkHQWqh4k,16310
|
|
204
204
|
zou/app/services/playlists_service.py,sha256=pAlPHET4jNdST5jsmJrFUkf1SVhfSoML9zdNpZ_88l4,32439
|
|
205
205
|
zou/app/services/preview_files_service.py,sha256=SIZ_SB1bWNRE_Zm7SuEpvFWqVpHKOgfZlCM2BfkgbWE,35598
|
|
206
206
|
zou/app/services/projects_service.py,sha256=_J8hIHy3MX5MsdEMRIKNfbyewwhxtMEcc_ymeHBsF38,21434
|
|
@@ -210,7 +210,7 @@ zou/app/services/shots_service.py,sha256=Y1oTewc2TNxwPzU5CZFfEhjJznjR54a9GTirFZx
|
|
|
210
210
|
zou/app/services/stats_service.py,sha256=cAlc92i9d6eYtsuwe3hYHYwdytg8KEMi1-TADfysJwM,11733
|
|
211
211
|
zou/app/services/status_automations_service.py,sha256=tVio7Sj7inhvKS4UOyRhcdpwr_KNP96hT1o0X7XcGF4,715
|
|
212
212
|
zou/app/services/sync_service.py,sha256=EunfXlma_IIb7011A_xLQLVQGAi-MteKgm2Y2NAxvMs,41586
|
|
213
|
-
zou/app/services/tasks_service.py,sha256=
|
|
213
|
+
zou/app/services/tasks_service.py,sha256=AZYz0qiDcX6t6q_ZZAdw7-6HzXziUux05G2GEtDDRLc,69347
|
|
214
214
|
zou/app/services/telemetry_services.py,sha256=xQm1h1t_JxSFW59zQGf4NuNdUi1UfMa_6pQ-ytRbmGA,1029
|
|
215
215
|
zou/app/services/time_spents_service.py,sha256=TBLC1O9Dg_UbciG5Nw-dejqX2-5n6q44lACeN6OnUkQ,15206
|
|
216
216
|
zou/app/services/user_service.py,sha256=BiOhPV7O-vowet7jOksjXk2V4yxZkdqsIyNboJ-Oz_A,46595
|
|
@@ -228,10 +228,11 @@ zou/app/utils/colors.py,sha256=LaGV17NL_8xY0XSp8snGWz5UMwGnm0KPWXyE5BTMG6w,200
|
|
|
228
228
|
zou/app/utils/commands.py,sha256=3TxXFOQxsWBT85DG1dVnuqdMbjFp58XJEtgYxhxbhhs,27304
|
|
229
229
|
zou/app/utils/csv_utils.py,sha256=difqBp9xp3nHvaOjW6B64A4X3Uyfe_vKGw10i6SJ5_E,1291
|
|
230
230
|
zou/app/utils/date_helpers.py,sha256=jFxDPCbAasg0I1gsC72AKEbGcx5c4pLqXZkSfZ4wLdQ,4724
|
|
231
|
-
zou/app/utils/dbhelpers.py,sha256=
|
|
231
|
+
zou/app/utils/dbhelpers.py,sha256=RSJuoxLexGJyME16GQCs-euFLBR0u-XAFdJ1KMSv5M8,1143
|
|
232
232
|
zou/app/utils/emails.py,sha256=6LgzRSV3wf2rkJiplOD2c79-o75X_ddpe63ht2xhTME,1433
|
|
233
233
|
zou/app/utils/env.py,sha256=haveriMt1rDeLOosAPMm0IblF_QpMViEsjg7BJqD-rw,711
|
|
234
234
|
zou/app/utils/events.py,sha256=a_W70v0Oi4QJwJzCLVUQ8RTwjlWo-VzAT_hQZCz2HQU,3024
|
|
235
|
+
zou/app/utils/fido.py,sha256=qhUWZdDCvgWqydYS4DN1SVkNwdAQkRSV9zs0Xw9O15E,536
|
|
235
236
|
zou/app/utils/fields.py,sha256=uZbKRqeG5_SmbOu3K0W-i-KmlTI1Y7X4rlKcVnu4DzI,3475
|
|
236
237
|
zou/app/utils/flask.py,sha256=t0sC6yEDyzn-vCsijWzI9lpNWoxAYWfui25QtjQi_64,1822
|
|
237
238
|
zou/app/utils/fs.py,sha256=TcPpuTq5u2B9Yyl-vieceuYOksF1oQnAnl2bCS4uya4,3234
|
|
@@ -241,6 +242,7 @@ zou/app/utils/monitoring.py,sha256=xOwyfM-7ZKWNtOxmX2WB1fOav5NpY6k8Tmuhli0nMBs,2
|
|
|
241
242
|
zou/app/utils/permissions.py,sha256=Oq91C_lN6aGVCtCVUqQhijMQEjXOiMezbngpjybzzQk,3426
|
|
242
243
|
zou/app/utils/query.py,sha256=Uwmco3NMpSlG6o7arQP52mx60Sq63T0DkUUKVsrkj-I,3126
|
|
243
244
|
zou/app/utils/remote_job.py,sha256=QPxcCWEv-NM1Q4IQawAyJAiSORwkMeOlByQb9OCShEw,2522
|
|
245
|
+
zou/app/utils/saml.py,sha256=b869bvaTR4E4CzOK2gbaN9h6JVkU6aVb1v8_F_ofWDY,1837
|
|
244
246
|
zou/app/utils/shell.py,sha256=D30NuOzr1D6jsa9OF69JYXyqYNhtIdnfKSVYW4irbz8,405
|
|
245
247
|
zou/app/utils/string.py,sha256=ZIJuQRe7ml669-T1o9keQtk_lV2D1DkS9ARQdexlQ6Y,404
|
|
246
248
|
zou/app/utils/thumbnail.py,sha256=eUb25So1fbjxZKYRpTOxLcZ0vww5zXNVkfVIa_tu5Dk,6625
|
|
@@ -374,7 +376,7 @@ zou/migrations/versions/c49e41f1298b_add_previewbackground.py,sha256=7TOXMsEK9YW
|
|
|
374
376
|
zou/migrations/versions/c68c2a62cfac_add_mimetype_column_to_attachment.py,sha256=cEFnRMYHurag8D9-7ycE34gnVH5TdsDfRYQqs5v_g8M,727
|
|
375
377
|
zou/migrations/versions/c726b98be194_.py,sha256=nbqWFmPg4OUCL-txrVjWyQiFm_PuuaDZeE2gOB-DM3w,701
|
|
376
378
|
zou/migrations/versions/c81f3e83bdb5_.py,sha256=eGoGIOImywjBRSqe-isbt_N_o3zK1ktrPWSo0nyAXgA,720
|
|
377
|
-
zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py,sha256=
|
|
379
|
+
zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py,sha256=adalMYr3AvOEpPR5V1nj6QxsVJUz-44VYaGnHM_tQyA,3046
|
|
378
380
|
zou/migrations/versions/cf3d365de164_add_entity_version_model.py,sha256=n3k3ojRQAk0tJAOBIUgDEoZbNFERx4vs3hg5hrFm7v4,1763
|
|
379
381
|
zou/migrations/versions/cf6cec6d6bf5_add_status_field_to_preview_file.py,sha256=0c8_OghAaaM0R6bKbs7MADvOkZ1sFE3SVp_nNB2ACgQ,950
|
|
380
382
|
zou/migrations/versions/d80267806131_task_status_new_column_is_default.py,sha256=7HtK0bfBUh9MrJIbpUgz6S-Ye_R_4DbHILpODMBVVwE,2610
|
|
@@ -408,9 +410,9 @@ zou/remote/normalize_movie.py,sha256=zNfEY3N1UbAHZfddGONTg2Sff3ieLVWd4dfZa1dpnes
|
|
|
408
410
|
zou/remote/playlist.py,sha256=AsDo0bgYhDcd6DfNRV6r6Jj3URWwavE2ZN3VkKRPbLU,3293
|
|
409
411
|
zou/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
410
412
|
zou/utils/movie.py,sha256=u9LCEOvmkxwm-KiZ6jKNdB9LSC6XXUDwJpVx8LkDwJg,16416
|
|
411
|
-
zou-0.19.
|
|
412
|
-
zou-0.19.
|
|
413
|
-
zou-0.19.
|
|
414
|
-
zou-0.19.
|
|
415
|
-
zou-0.19.
|
|
416
|
-
zou-0.19.
|
|
413
|
+
zou-0.19.51.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
|
414
|
+
zou-0.19.51.dist-info/METADATA,sha256=02lquTsHAGZfgNpL9MpOJgfAgc_izndXajgD5Jh3Q-U,6704
|
|
415
|
+
zou-0.19.51.dist-info/WHEEL,sha256=ixB2d4u7mugx_bCBycvM9OzZ5yD7NmPXFRtKlORZS2Y,91
|
|
416
|
+
zou-0.19.51.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
|
|
417
|
+
zou-0.19.51.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
|
|
418
|
+
zou-0.19.51.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|