zou 0.19.50__py3-none-any.whl → 0.19.52__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
zou/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.19.50"
1
+ __version__ = "0.19.52"
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,112 @@ 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
+ response = make_response(
1380
+ redirect(f"{config.DOMAIN_PROTOCOL}://{config.DOMAIN_NAME}")
1381
+ )
1382
+
1383
+ if user["active"]:
1384
+ access_token = create_access_token(
1385
+ identity=user["id"],
1386
+ additional_claims={
1387
+ "identity_type": "person",
1388
+ },
1389
+ )
1390
+ refresh_token = create_refresh_token(
1391
+ identity=user["id"],
1392
+ additional_claims={
1393
+ "identity_type": "person",
1394
+ },
1395
+ )
1396
+ identity_changed.send(
1397
+ current_app._get_current_object(),
1398
+ identity=Identity(user["id"], "person"),
1399
+ )
1400
+
1401
+ ip_address = request.environ.get(
1402
+ "HTTP_X_REAL_IP", request.remote_addr
1403
+ )
1404
+
1405
+ set_access_cookies(response, access_token)
1406
+ set_refresh_cookies(response, refresh_token)
1407
+ events_service.create_login_log(user["id"], ip_address, "web")
1408
+
1409
+ return response
1410
+
1411
+
1412
+ class SAMLLoginResource(Resource, ArgsMixin):
1413
+ """
1414
+ Resource to allow a user to login with SAML SSO.
1415
+ """
1416
+
1417
+ def get(self):
1418
+ """
1419
+ Resource to allow a user to login with SAML SSO.
1420
+ ---
1421
+ description: ""
1422
+ tags:
1423
+ - Authentication
1424
+ responses:
1425
+ 302:
1426
+ description: Redirect to the SAML IDP.
1427
+ 400:
1428
+ description: Wrong parameter.
1429
+ """
1430
+ if not config.SAML_ENABLED:
1431
+ return {"error": "SAML is not enabled."}, 400
1432
+ _, info = current_app.extensions[
1433
+ "saml_client"
1434
+ ].prepare_for_authenticate()
1435
+
1436
+ redirect_url = None
1437
+
1438
+ # Select the IdP URL to send the AuthN request to
1439
+ for key, value in info["headers"]:
1440
+ if key == "Location":
1441
+ redirect_url = value
1442
+
1443
+ 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, date_helpers
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
@@ -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": app.config["APP_NAME"], "version": __version__}
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
- api_name,
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
- config = {
285
- "is_self_hosted": app.config["IS_SELF_HOSTED"],
286
- "crisp_token": app.config["CRISP_TOKEN"],
282
+ conf = {
283
+ "is_self_hosted": config.IS_SELF_HOSTED,
284
+ "crisp_token": config.CRISP_TOKEN,
287
285
  "indexer_configured": (
288
- len(app.config["INDEXER"]["key"]) > 0
289
- and app.config["INDEXER"]["key"] != "masterkey"
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 app.config["SENTRY_KITSU_ENABLED"]:
293
- config["sentry"] = {
294
- "dsn": app.config["SENTRY_KITSU_DSN"],
295
- "sampleRate": app.config["SENTRY_KITSU_SR"],
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 config
297
+ return conf
298
298
 
299
299
 
300
300
  class TestEventsResource(Resource):
@@ -76,6 +76,7 @@ ALLOWED_FILE_EXTENSION = [
76
76
  "sbbkp",
77
77
  "svg",
78
78
  "swf",
79
+ "tvpp",
79
80
  "wav",
80
81
  "zip",
81
82
  ]
@@ -980,6 +981,7 @@ class BaseThumbnailResource(Resource):
980
981
 
981
982
 
982
983
  class PersonThumbnailResource(BaseThumbnailResource):
984
+
983
985
  def __init__(self):
984
986
  BaseThumbnailResource.__init__(
985
987
  self,
@@ -1027,6 +1029,7 @@ class ProjectThumbnailResource(BaseThumbnailResource):
1027
1029
  "projects",
1028
1030
  projects_service.get_project,
1029
1031
  projects_service.update_project,
1032
+ thumbnail_utils.BIG_SQUARE_SIZE,
1030
1033
  )
1031
1034
 
1032
1035
  def check_allowed_to_get(self, instance_id):
@@ -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 WrongParameterException
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
- return tasks_service.get_tasks_for_project(project_id, page)
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)
@@ -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
- from urllib.parse import urlparse
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(state, registration_response)
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 person.email in config.PROTECTED_ACCOUNTS:
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(project_id, page=0):
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
 
@@ -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", required=True)
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(person["id"], {"role": "admin"})
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":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zou
3
- Version: 0.19.50
3
+ Version: 0.19.52
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.8.1, <3.13
21
+ Requires-Python: >=3.9, <3.13
23
22
  License-File: LICENSE
24
- Requires-Dist: babel ==2.15.0
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.13
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 ==22.0.0
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.4
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.6
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.7
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.31
70
+ Requires-Dist: sqlalchemy ==2.0.32
71
71
  Requires-Dist: ua-parser ==0.18.0
72
- Requires-Dist: werkzeug ==3.0.3
73
- Requires-Dist: numpy ==1.24.4 ; python_version == "3.8"
74
- Requires-Dist: numpy ==2.0.1 ; python_version >= "3.9"
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.4.2 ; extra == 'lint'
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.11.0 ; extra == 'monitoring'
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.23.3 ; extra == 'test'
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=_Gy_AT8DQ-udpLfSZgx_ouKt88H4OjNcTtS35Oi8b7I,24
2
- zou/cli.py,sha256=2cDkbEOqp_m9hzBQf5wpxc_h0WjoH8KtxQQMNuREYlc,18201
1
+ zou/__init__.py,sha256=CXdAtGdLQ3H3JntZq8Z7OptAeL5sbsEvqWtL7mYMKTo,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=aWh9K5n63TpXdLDxpfFtNNoCYZVE6OV3YLCXLuygWQg,6700
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=J0jmGmyvk8JqcMPAEW8KfvwP8GwEoC9BxQG8AQV9aiY,6393
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=b8Syg8DHJn5s3avMZo5ukNQCzpX_2z9fmBSG3EIt5Yg,998
15
- zou/app/blueprints/auth/resources.py,sha256=b6zPYiBE_a1gE2ei3FFswy1Iml2r6TtMB5i48f9mPKk,41760
14
+ zou/app/blueprints/auth/__init__.py,sha256=xP874bMWUnLIirOPSEbpe-Q2fBBQrxZGKd0tLZmNJXk,1128
15
+ zou/app/blueprints/auth/resources.py,sha256=CAqlC8Q2Mw3grjL2bU_aZJQ18huwa1Kf3ym2sueH8Bg,45136
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=Vsa3PFTT8L2x-FP9JXo4fYecWx-dRCzd8049V3g865w,8313
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=x9sjL7cEyEK9yrAXUahWenVumPqJM4YDJ40jpu37ljE,4400
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=DsYpNSO_wCAxyjWOaB9QmEqOBcjn1miLiGnfY-ii1z8,8165
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=SrbAcsHizZZiWOjnDGurYmuQlxkj1KDyXKE85x_Wz5M,47062
94
+ zou/app/blueprints/previews/resources.py,sha256=UgzObCvYfQTT1c1OriQaWVYr17iuQQpbrLHZThsKcNM,47120
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=H9-TbHod5e7w6fO-1-C81Npk2reJkfiqTjRe7KgjaLI,48669
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=tx7h2mAJdfiXnbMXX2-EEndpw8ExcXYQqb-7mPT3bjs,55290
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=AxKN_THkjN2tmOHTSyFspwwFHqGBnslXuidfbav8Rxk,23224
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=MukM-mbmrAglQGcvDLoUVdzBXvCSgepUMAaeODIrT2E,16219
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=Y7VlDJon41m3wjMSOqvoxo0y7UDxlI7NuFPprRMySU0,68602
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=edWZgt9IAwg-8Y1SPh_c1mYLrnCy-EY_adONNsQd4yo,852
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
@@ -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.50.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
412
- zou-0.19.50.dist-info/METADATA,sha256=QQwGEWHpVwuuwp5RgWo0k6rFY2flwv1AHlRLtDDJ4xo,6725
413
- zou-0.19.50.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
414
- zou-0.19.50.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
415
- zou-0.19.50.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
416
- zou-0.19.50.dist-info/RECORD,,
413
+ zou-0.19.52.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
414
+ zou-0.19.52.dist-info/METADATA,sha256=60v2YWNqsw0AW9QBQdYFaQ-XBUfXMaGBbp_p0KxL6-g,6704
415
+ zou-0.19.52.dist-info/WHEEL,sha256=uCRv0ZEik_232NlR4YDw4Pv3Ajt5bKvMH13NUU7hFuI,91
416
+ zou-0.19.52.dist-info/entry_points.txt,sha256=PelQoIx3qhQ_Tmne7wrLY-1m2izuzgpwokoURwSohy4,130
417
+ zou-0.19.52.dist-info/top_level.txt,sha256=4S7G_jk4MzpToeDItHGjPhHx_fRdX52zJZWTD4SL54g,4
418
+ zou-0.19.52.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (74.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
File without changes