diff --git a/setup.cfg b/setup.cfg index 2717bfd313..edd8bcd853 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,7 @@ install_requires = matterhook==0.2 meilisearch==0.28.3 OpenTimelineIO==0.15.0 + orjson==3.9.7 pillow==10.0.1 psutil==5.9.5 psycopg[binary]==3.1.11 diff --git a/tests/auth/test_auth_route.py b/tests/auth/test_auth_route.py index e2e11c5f86..30a7b7de33 100644 --- a/tests/auth/test_auth_route.py +++ b/tests/auth/test_auth_route.py @@ -1,4 +1,4 @@ -import json +import orjson as json from tests.base import ApiDBTestCase diff --git a/tests/base.py b/tests/base.py index f96a35988c..4bee6867d2 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,6 +1,6 @@ import datetime import unittest -import json +import orjson as json import os import ntpath diff --git a/tests/misc/test_commands.py b/tests/misc/test_commands.py index 2902b92dca..1970d49c8a 100644 --- a/tests/misc/test_commands.py +++ b/tests/misc/test_commands.py @@ -1,4 +1,4 @@ -import json +import orjson as json import datetime from tests.base import ApiDBTestCase @@ -34,7 +34,7 @@ def test_clean_auth_tokens_revoked(self): }, "revoked": False, } - ).encode("utf-8"), + ), ) self.store.add( "testkey2", @@ -45,7 +45,7 @@ def test_clean_auth_tokens_revoked(self): }, "revoked": True, } - ).encode("utf-8"), + ), ) self.assertEqual(len(self.store.keys()), 2) commands.clean_auth_tokens() @@ -63,7 +63,7 @@ def test_clean_auth_tokens_expired(self): }, "revoked": False, } - ).encode("utf-8"), + ), ) self.store.add( "testkey2", @@ -74,7 +74,7 @@ def test_clean_auth_tokens_expired(self): }, "revoked": False, } - ).encode("utf-8"), + ), ) self.assertEqual(len(self.store.keys()), 2) diff --git a/tests/source/csv/test_import_assets.py b/tests/source/csv/test_import_assets.py index 82383e74c7..6ebb93862c 100644 --- a/tests/source/csv/test_import_assets.py +++ b/tests/source/csv/test_import_assets.py @@ -1,5 +1,5 @@ import os -import json +import orjson as json from tests.base import ApiDBTestCase from zou.app import db diff --git a/tests/source/shotgun/base.py b/tests/source/shotgun/base.py index b01add53ad..4db73a8cda 100644 --- a/tests/source/shotgun/base.py +++ b/tests/source/shotgun/base.py @@ -1,4 +1,4 @@ -import json +import orjson as json from tests.base import ApiDBTestCase diff --git a/zou/app/__init__.py b/zou/app/__init__.py index 3a832411e4..2dd2852471 100644 --- a/zou/app/__init__.py +++ b/zou/app/__init__.py @@ -29,10 +29,11 @@ from zou.app.utils import cache, fs, logs from zou.app.utils.sentry import init_sentry -from zou.app.utils.user_agent import ParsedUserAgent +from zou.app.utils.flask import ParsedUserAgent, ORJSONProvider init_sentry() app = Flask(__name__) +app.json = ORJSONProvider(app) app.request_class.user_agent_class = ParsedUserAgent app.config.from_object(config) diff --git a/zou/app/blueprints/auth/resources.py b/zou/app/blueprints/auth/resources.py index 00799a0de5..b7587eac4f 100644 --- a/zou/app/blueprints/auth/resources.py +++ b/zou/app/blueprints/auth/resources.py @@ -83,9 +83,7 @@ def wrong_auth_handler(identity_user=None): @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): - if identity.id is not None: - from zou.app.services import persons_service - + if isinstance(identity.id, str): try: identity.user = persons_service.get_person(identity.id) diff --git a/zou/app/blueprints/comments/resources.py b/zou/app/blueprints/comments/resources.py index 2a1dcf66b4..9559464ca8 100644 --- a/zou/app/blueprints/comments/resources.py +++ b/zou/app/blueprints/comments/resources.py @@ -1,4 +1,4 @@ -import json +import orjson as json from flask import abort, request, send_file as flask_send_file from flask_restful import Resource, reqparse diff --git a/zou/app/blueprints/crud/base.py b/zou/app/blueprints/crud/base.py index 5d351d0ffd..7a212e5470 100644 --- a/zou/app/blueprints/crud/base.py +++ b/zou/app/blueprints/crud/base.py @@ -1,5 +1,5 @@ import math -import json +import orjson as json import sqlalchemy.orm as orm from flask import request, abort, current_app diff --git a/zou/app/config.py b/zou/app/config.py index d818dea4ef..3f47cb152c 100644 --- a/zou/app/config.py +++ b/zou/app/config.py @@ -6,7 +6,6 @@ from zou.app.utils.env import envtobool, env_with_semicolon_to_list PROPAGATE_EXCEPTIONS = True -RESTFUL_JSON = {"ensure_ascii": False} DEBUG = envtobool("DEBUG", False) DEBUG_PORT = int(os.getenv("DEBUG_PORT", 5000)) diff --git a/zou/app/services/file_tree_service.py b/zou/app/services/file_tree_service.py index 9b4dcd09cf..83f547acbd 100644 --- a/zou/app/services/file_tree_service.py +++ b/zou/app/services/file_tree_service.py @@ -1,6 +1,6 @@ import os import re -import json +import orjson as json from collections import OrderedDict from slugify import slugify diff --git a/zou/app/services/playlists_service.py b/zou/app/services/playlists_service.py index 0c8688f720..6c54b13e78 100644 --- a/zou/app/services/playlists_service.py +++ b/zou/app/services/playlists_service.py @@ -1,6 +1,6 @@ import base64 -import json +import orjson as json import os import zlib diff --git a/zou/app/utils/api.py b/zou/app/utils/api.py index e7bc9d0e3e..f4369f1196 100644 --- a/zou/app/utils/api.py +++ b/zou/app/utils/api.py @@ -1,4 +1,5 @@ -from flask_restful import Api, output_json +from flask_restful import Api +from zou.app.utils.flask import output_json def configure_api_from_blueprint(blueprint, route_tuples): @@ -13,7 +14,6 @@ def configure_api_from_blueprint(blueprint, route_tuples): api = Api(blueprint, catch_all_404s=True) api.representations = { - "application/json; charset=utf-8": output_json, "application/json": output_json, } diff --git a/zou/app/utils/commands.py b/zou/app/utils/commands.py index e808b7ae9a..6003abee8f 100644 --- a/zou/app/utils/commands.py +++ b/zou/app/utils/commands.py @@ -1,7 +1,7 @@ # coding: utf-8 import os -import json +import orjson as json import datetime import tempfile diff --git a/zou/app/utils/fields.py b/zou/app/utils/fields.py index b6f460aa3b..807ec83d08 100644 --- a/zou/app/utils/fields.py +++ b/zou/app/utils/fields.py @@ -26,10 +26,6 @@ def serialize_value(value): return serialize_orm_arrays(value) elif isinstance(value, bytes): return value.decode("utf-8") - elif isinstance(value, str): - return value - elif isinstance(value, int): - return value elif isinstance(value, list): return serialize_list(value) elif isinstance(value, Locale): diff --git a/zou/app/utils/flask.py b/zou/app/utils/flask.py new file mode 100644 index 0000000000..990f861d58 --- /dev/null +++ b/zou/app/utils/flask.py @@ -0,0 +1,51 @@ +from ua_parser import user_agent_parser +from werkzeug.user_agent import UserAgent +from werkzeug.utils import cached_property +from flask.json.provider import JSONProvider +from flask import make_response +import orjson + +orjson_options = orjson.OPT_NON_STR_KEYS + + +def output_json(data, code, headers=None): + """Makes a Flask response with a JSON encoded body""" + dumped = orjson.dumps(data, option=orjson_options) + + resp = make_response(dumped, code) + resp.headers.extend(headers or {}) + return resp + + +class ORJSONProvider(JSONProvider): + def __init__(self, *args, **kwargs): + self.options = kwargs + super().__init__(*args, **kwargs) + + def loads(self, s, **kwargs): + return orjson.loads(s) + + def dumps(self, obj, **kwargs): + return orjson.dumps(obj, option=orjson_options).decode("utf-8") + + +class ParsedUserAgent(UserAgent): + @cached_property + def _details(self): + return user_agent_parser.Parse(self.string) + + @property + def platform(self): + return self._details["os"]["family"] + + @property + def browser(self): + return self._details["user_agent"]["family"] + + @property + def version(self): + return ".".join( + part + for key in ("major", "minor", "patch") + if (part := self._details["user_agent"][key]) is not None + ) diff --git a/zou/app/utils/remote_job.py b/zou/app/utils/remote_job.py index 26916d2e5b..9e58e95fb6 100644 --- a/zou/app/utils/remote_job.py +++ b/zou/app/utils/remote_job.py @@ -1,6 +1,6 @@ import nomad import base64 -import json +import orjson as json import textwrap import time diff --git a/zou/app/utils/user_agent.py b/zou/app/utils/user_agent.py deleted file mode 100644 index c4891c4bc0..0000000000 --- a/zou/app/utils/user_agent.py +++ /dev/null @@ -1,25 +0,0 @@ -from ua_parser import user_agent_parser -from werkzeug.user_agent import UserAgent -from werkzeug.utils import cached_property - - -class ParsedUserAgent(UserAgent): - @cached_property - def _details(self): - return user_agent_parser.Parse(self.string) - - @property - def platform(self): - return self._details["os"]["family"] - - @property - def browser(self): - return self._details["user_agent"]["family"] - - @property - def version(self): - return ".".join( - part - for key in ("major", "minor", "patch") - if (part := self._details["user_agent"][key]) is not None - ) diff --git a/zou/event_stream.py b/zou/event_stream.py index a53e5e17a4..b0d7cc3edf 100644 --- a/zou/event_stream.py +++ b/zou/event_stream.py @@ -13,6 +13,7 @@ from zou.app import config from zou.app.stores import auth_tokens_store from zou.app.utils.sentry import init_sentry +from zou.app.utils.flask import ORJSONProvider server_stats = {"nb_connections": 0} rooms_data = {} @@ -216,6 +217,7 @@ def create_app(): ) init_sentry() app = Flask(__name__) + app.json = ORJSONProvider(app) app.config.from_object(config) set_info_routes(socketio, app) set_application_routes(socketio, app) diff --git a/zou/remote/config_payload.py b/zou/remote/config_payload.py index 804cc675b6..c0d72f6d4f 100644 --- a/zou/remote/config_payload.py +++ b/zou/remote/config_payload.py @@ -1,5 +1,5 @@ import os -import json +import orjson as json import sys from pathlib import Path diff --git a/zou/remote/playlist.py b/zou/remote/playlist.py index 47694e3952..e2188701c6 100755 --- a/zou/remote/playlist.py +++ b/zou/remote/playlist.py @@ -1,6 +1,6 @@ #!/usr/bin/env python import base64 -import json +import orjson as json import logging import os import sys