diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 07ff6bcee..b04d28e99 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -42,6 +42,7 @@ jobs: python-version: [3.7, 3.8] database: ["DUMMY", "postgres", "cockroachdb"] db_schema: ["custom", "public"] + db_serializer: ["pickle", "json"] exclude: - database: "DUMMY" db_schema: "custom" @@ -52,6 +53,7 @@ jobs: env: DATABASE: ${{ matrix.database }} DB_SCHEMA: ${{ matrix.db_schema }} + DB_SERIALIZER: ${{ matrix.db_serializer }} steps: - name: Checkout the repository diff --git a/contrib-requirements.txt b/contrib-requirements.txt index c0517dc57..2d9c4814a 100644 --- a/contrib-requirements.txt +++ b/contrib-requirements.txt @@ -5,8 +5,7 @@ aiosmtplib==1.0.6 pre-commit==1.18.2 flake8==3.7.7 codecov==2.0.15 -mypy==0.720 -mypy-zope==0.2.0 +mypy-zope==0.2.8 black==19.10b0 isort==4.3.21 jinja2==2.11.1 \ No newline at end of file diff --git a/guillotina/_settings.py b/guillotina/_settings.py index 42c3efec2..273d75e90 100644 --- a/guillotina/_settings.py +++ b/guillotina/_settings.py @@ -91,7 +91,7 @@ "check_writable_request": "guillotina.writable.check_writable_request", "indexer": "guillotina.catalog.index.Indexer", "search_parser": "default", - "object_reader": "guillotina.db.reader.reader", + "db_serializer": "pickle", "thread_pool_workers": 32, "server_settings": {"uvicorn": {"timeout_keep_alive": 5, "http": "h11"}}, "valid_id_characters": string.digits + string.ascii_lowercase + ".-_@$^()+ =", diff --git a/guillotina/contrib/catalog/pg.py b/guillotina/contrib/catalog/pg.py index ca863c463..1234d86fb 100644 --- a/guillotina/contrib/catalog/pg.py +++ b/guillotina/contrib/catalog/pg.py @@ -9,6 +9,7 @@ from guillotina.catalog.utils import get_index_definition from guillotina.catalog.utils import iter_indexes from guillotina.catalog.utils import parse_query +from guillotina.component import get_adapter from guillotina.component import get_utility from guillotina.const import TRASHED_ID from guillotina.db.interfaces import IPostgresStorage @@ -706,7 +707,7 @@ async def _process_object(self, obj, data): uuid = obj.__uuid__ - writer = IWriter(obj) + writer = get_adapter(obj, IWriter, name=app_settings["db_serializer"]) await self._index(uuid, writer, data["transaction"], data["table_name"]) data["count"] += 1 diff --git a/guillotina/db/interfaces.py b/guillotina/db/interfaces.py index 047f834b0..903721e9b 100644 --- a/guillotina/db/interfaces.py +++ b/guillotina/db/interfaces.py @@ -17,6 +17,10 @@ class IWriter(Interface): """Serializes the object for DB storage""" +class IReader(Interface): + """Deserializes the object from DB storage""" + + class ITransaction(Interface): _db_conn = Attribute("") _query_count_end = Attribute("") diff --git a/guillotina/db/serializers/__init__.py b/guillotina/db/serializers/__init__.py new file mode 100644 index 000000000..50dcc73e8 --- /dev/null +++ b/guillotina/db/serializers/__init__.py @@ -0,0 +1,2 @@ +from . import json # noqa +from . import pickle # noqa diff --git a/guillotina/db/serializers/json/__init__.py b/guillotina/db/serializers/json/__init__.py new file mode 100644 index 000000000..1b8e20df1 --- /dev/null +++ b/guillotina/db/serializers/json/__init__.py @@ -0,0 +1,4 @@ +from .reader import * # noqa +from .value_deserializers import * # noqa +from .value_serializers import * # noqa +from .writer import * # noqa diff --git a/guillotina/db/serializers/json/interfaces.py b/guillotina/db/serializers/json/interfaces.py new file mode 100644 index 000000000..228b6c12e --- /dev/null +++ b/guillotina/db/serializers/json/interfaces.py @@ -0,0 +1,9 @@ +from zope.interface import Interface + + +class IStorageDeserializer(Interface): + pass + + +class IStorageSerializer(Interface): + pass diff --git a/guillotina/db/serializers/json/reader.py b/guillotina/db/serializers/json/reader.py new file mode 100644 index 000000000..99c774d2a --- /dev/null +++ b/guillotina/db/serializers/json/reader.py @@ -0,0 +1,47 @@ +from .interfaces import IStorageDeserializer +from guillotina import configure +from guillotina.component import get_adapter +from guillotina.component import query_adapter +from guillotina.db.interfaces import IReader +from guillotina.db.orm.interfaces import IBaseObject +from guillotina.utils import resolve_dotted_name +from zope.interface.interface import Interface + +import asyncpg +import orjson + + +def recursive_load(d): + if isinstance(d, dict): + if "__class__" in d: + adapter = query_adapter(d, IStorageDeserializer, name=d["__class__"]) + if adapter is None: + adapter = get_adapter(d, IStorageDeserializer, name="$.AnyObjectDict") + return adapter() + else: + for k, v in d.items(): + d[k] = recursive_load(v) + return d + elif isinstance(d, list): + return [recursive_load(v) for v in d] + else: + return d + + +@configure.adapter(for_=(Interface), provides=IReader, name="json") +def reader(result_: asyncpg.Record) -> IBaseObject: + result = dict(result_) + state = orjson.loads(result["state"]) + dotted_class = state.pop("__class__") + type_class = resolve_dotted_name(dotted_class) + obj = type_class.__new__(type_class) + + state = recursive_load(state) + + obj.__dict__.update(state) + obj.__uuid__ = result["zoid"] + obj.__serial__ = result["tid"] + obj.__name__ = result["id"] + obj.__provides__ = obj.__providedBy__ + + return obj diff --git a/guillotina/db/serializers/json/value_deserializers.py b/guillotina/db/serializers/json/value_deserializers.py new file mode 100644 index 000000000..f8341473e --- /dev/null +++ b/guillotina/db/serializers/json/value_deserializers.py @@ -0,0 +1,128 @@ +from .interfaces import IStorageDeserializer +from .reader import recursive_load +from dateutil.parser import parse +from guillotina import configure +from guillotina.interfaces.security import PermissionSetting +from guillotina.security.securitymap import SecurityMap +from guillotina.utils import resolve_dotted_name +from zope.interface import Interface +from zope.interface.declarations import Implements + +import base64 +import pickle + + +class PickleDeserializer: + def __init__(self, data): + self.data = data + + def __call__(self): + return pickle.loads(base64.b64decode(self.data["__pickle__"])) + + +@configure.adapter( + for_=Interface, provides=IStorageDeserializer, name="$.AnyObjectDict", +) +class AnyObjectDeserializer: + def __init__(self, data): + self.data = data + + def __call__(self): + klass = self.data.pop("__class__") + type_class = resolve_dotted_name(klass) + obj = type_class.__new__(type_class) + obj.__dict__ = recursive_load(self.data) + return obj + + +@configure.adapter( + for_=Interface, provides=IStorageDeserializer, name="guillotina.fields.annotation.BucketListValue", +) +class BucketListValueDeserializer(PickleDeserializer): + pass + + +@configure.adapter( + for_=Interface, provides=IStorageDeserializer, name="guillotina.blob.Blob", +) +class BlobDeserializer(PickleDeserializer): + pass + + +@configure.adapter(for_=Interface, provides=IStorageDeserializer, name="builtins.set") +@configure.adapter(for_=Interface, provides=IStorageDeserializer, name="builtins.frozenset") +class SetDeseializer: + def __init__(self, data): + self.data = data + + def __call__(self): + data = self.data + if data["__class__"] == "builtins.set": + return set(data["__value__"]) + else: + return frozenset(data["__value__"]) + + +@configure.adapter(for_=Interface, provides=IStorageDeserializer, name="datetime.datetime") +class DatetimeDeserializer: + def __init__(self, data): + self.data = data + + def __call__(self): + return parse(self.data["__value__"]) + + +@configure.adapter( + for_=Interface, provides=IStorageDeserializer, name="zope.interface.declarations.Implements", +) +class ImplementsDeseializer: + def __init__(self, data): + self.data = data + + def __call__(self): + data = self.data + obj = Implements() + obj.__bases__ = [resolve_dotted_name(iface) for iface in data["__ifaces__"]] + obj.__name__ = data["__name__"] + return obj + + +@configure.adapter( + for_=Interface, provides=IStorageDeserializer, name="zope.interface.Provides", +) +class ProvidesDeserializer: + def __init__(self, data): + self.data = data + + def __call__(self): + # from zope.interface import Provides # type: ignore + # obj = Provides(*[resolve_dotted_name(iface) for iface in self.data["__ifaces__"]]) + # return obj + return pickle.loads(base64.b64decode(self.data["__pickle__"])) + + +@configure.adapter( + for_=Interface, provides=IStorageDeserializer, name="guillotina.security.securitymap.SecurityMap", +) +class SecurityMapDeserializer: + def __init__(self, data): + self.data = data + + def __call__(self): + sec_map = SecurityMap.__new__(SecurityMap) + sec_map.__dict__.update( + { + # byrow + k: { + # Role + k2: { + # Principal + k3: PermissionSetting(v3) + for k3, v3 in v2.items() + } + for k2, v2 in v.items() + } + for k, v in self.data["__dict__"].items() + } + ) + return sec_map diff --git a/guillotina/db/serializers/json/value_serializers.py b/guillotina/db/serializers/json/value_serializers.py new file mode 100644 index 000000000..524694b0b --- /dev/null +++ b/guillotina/db/serializers/json/value_serializers.py @@ -0,0 +1,127 @@ +from .interfaces import IStorageSerializer +from guillotina import configure +from guillotina.utils import get_dotted_name +from zope.interface import Interface + +import base64 +import pickle + + +class PickleSerializer: + dotted_name: str + + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + "__class__": self.dotted_name, + "__pickle__": base64.b64encode(pickle.dumps(self.obj)).decode(), + } + + +@configure.adapter( + for_=Interface, provides=IStorageSerializer, name="$.AnyObjectDict", +) +class AnyObjectSerializer: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + **{"__class__": get_dotted_name(self.obj)}, + **self.obj.__dict__, + } + + +@configure.adapter( + for_=Interface, provides=IStorageSerializer, name="guillotina.fields.annotation.BucketListValue", +) +class BucketListValueSerializer(PickleSerializer): + """ + The internal structure of the BucketListValue contains a dictionary + whose keys are of type other than string + """ + + dotted_name = "guillotina.fields.annotation.BucketListValue" + + +@configure.adapter( + for_=Interface, provides=IStorageSerializer, name="guillotina.blob.Blob", +) +class BlobSerializer(PickleSerializer): + """ + The internal structure of the Blob contains a dictionary + whose keys are of type other than string + """ + + dotted_name = "guillotina.blob.Blob" + + +@configure.adapter(for_=Interface, provides=IStorageSerializer, name="builtins.set") +@configure.adapter(for_=Interface, provides=IStorageSerializer, name="builtins.frozenset") +class SetSerializer: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + "__class__": get_dotted_name(type(self.obj)), + "__value__": list(self.obj), + } + + +@configure.adapter(for_=Interface, provides=IStorageSerializer, name="datetime.datetime") +class DatetimeSerializer: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + "__class__": get_dotted_name(type(self.obj)), + "__value__": self.obj.isoformat(), + } + + +@configure.adapter( + for_=Interface, provides=IStorageSerializer, name="zope.interface.declarations.Implements", +) +class ImplementsSerializer: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + "__class__": "zope.interface.declarations.Implements", + "__name__": self.obj.__name__, + "__ifaces__": [i.__identifier__ for i in self.obj.flattened()], + } + + +@configure.adapter( + for_=Interface, provides=IStorageSerializer, name="zope.interface.Provides", +) +class ProvidesSerializer: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + "__class__": "zope.interface.Provides", + # "__ifaces__": [i.__identifier__ for i in self.obj.flattened()], + "__pickle__": base64.b64encode(pickle.dumps(self.obj)).decode(), + } + + +@configure.adapter( + for_=Interface, provides=IStorageSerializer, name="guillotina.security.securitymap.SecurityMap", +) +class SecurityMapSerializer: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return { + "__class__": "guillotina.security.securitymap.SecurityMap", + "__dict__": self.obj.__dict__, + } diff --git a/guillotina/db/serializers/json/writer.py b/guillotina/db/serializers/json/writer.py new file mode 100644 index 000000000..031bf75b6 --- /dev/null +++ b/guillotina/db/serializers/json/writer.py @@ -0,0 +1,80 @@ +from .interfaces import IStorageSerializer +from guillotina import configure +from guillotina.component import get_adapter +from guillotina.component import query_adapter +from guillotina.db.interfaces import IWriter +from guillotina.db.serializers.pickle import writer +from guillotina.interfaces import IAnnotationData +from guillotina.interfaces import IDatabase +from guillotina.interfaces import IResource +from guillotina.interfaces.security import PermissionSetting +from guillotina.utils import get_dotted_name +from zope.interface.interface import InterfaceClass + +import orjson + + +def json_default(obj): + if type(obj) == str: + return obj + elif isinstance(obj, complex): + return [obj.real, obj.imag] + elif isinstance(obj, type): + return obj.__module__ + "." + obj.__name__ + elif isinstance(obj, InterfaceClass): + return [x.__module__ + "." + x.__name__ for x in obj.__iro__] # noqa + elif type(obj) == dict: + return obj + elif isinstance(obj, PermissionSetting): + return obj.get_name() + + dotted_name = get_dotted_name(type(obj)) + adapter = query_adapter(obj, IStorageSerializer, name=dotted_name) + if adapter is None: + adapter = get_adapter(obj, IStorageSerializer, name="$.AnyObjectDict") + return adapter() + + +@configure.adapter(for_=(IResource), provides=IWriter, name="json") +class Writer(writer.ResourceWriter): + def serialize(self): + d = { + **self._obj.__dict__, + **{ + "type_name": self._obj.type_name, + "__class__": get_dotted_name(self._obj), + "__behaviors__": self._obj.__behaviors__, + "__providedBy__": self._obj.__providedBy__, + "__implemented__": self._obj.__implemented__, + }, + } + d.pop("__provides__", None) + return orjson.dumps(d, default=json_default, option=orjson.OPT_PASSTHROUGH_DATETIME) + + +@configure.adapter(for_=(IAnnotationData), provides=IWriter, name="json") +class AnnotationWriter(writer.Writer): + def serialize(self): + d = { + **self._obj.__dict__, + **{ + "__class__": get_dotted_name(self._obj), + "__providedBy__": self._obj.__providedBy__, + "__implemented__": self._obj.__implemented__, + }, + } + return orjson.dumps(d, default=json_default, option=orjson.OPT_PASSTHROUGH_DATETIME) + + +@configure.adapter(for_=(IDatabase), provides=IWriter, name="json") +class DatabaseWriter(writer.Writer): + def serialize(self): + d = { + **self._obj.__dict__, + **{ + "__class__": get_dotted_name(self._obj), + "__providedBy__": self._obj.__providedBy__, + "__implemented__": self._obj.__implemented__, + }, + } + return orjson.dumps(d, default=json_default, option=orjson.OPT_PASSTHROUGH_DATETIME) diff --git a/guillotina/db/serializers/pickle/__init__.py b/guillotina/db/serializers/pickle/__init__.py new file mode 100644 index 000000000..29ebc0e28 --- /dev/null +++ b/guillotina/db/serializers/pickle/__init__.py @@ -0,0 +1,2 @@ +from .reader import * # noqa +from .writer import * # noqa diff --git a/guillotina/db/reader.py b/guillotina/db/serializers/pickle/reader.py similarity index 61% rename from guillotina/db/reader.py rename to guillotina/db/serializers/pickle/reader.py index 0e2d55f20..2a2f3366b 100644 --- a/guillotina/db/reader.py +++ b/guillotina/db/serializers/pickle/reader.py @@ -1,9 +1,13 @@ +from guillotina import configure +from guillotina.db.interfaces import IReader from guillotina.db.orm.interfaces import IBaseObject +from zope.interface.interface import Interface import pickle import typing +@configure.adapter(for_=(Interface), provides=IReader, name="pickle") def reader(result: dict) -> IBaseObject: obj = typing.cast(IBaseObject, pickle.loads(result["state"])) obj.__uuid__ = result["zoid"] diff --git a/guillotina/db/writer.py b/guillotina/db/serializers/pickle/writer.py similarity index 94% rename from guillotina/db/writer.py rename to guillotina/db/serializers/pickle/writer.py index 607184cba..041c9f2e6 100644 --- a/guillotina/db/writer.py +++ b/guillotina/db/serializers/pickle/writer.py @@ -29,7 +29,7 @@ async def __call__(self): return data -@configure.adapter(for_=(IBaseObject), provides=IWriter) +@configure.adapter(for_=(IBaseObject), provides=IWriter, name="pickle") class Writer(object): resource = False @@ -71,7 +71,7 @@ def id(self): return getattr(self._obj, "id", None) -@configure.adapter(for_=(IResource), provides=IWriter) +@configure.adapter(for_=(IResource), provides=IWriter, name="pickle") class ResourceWriter(Writer): resource = True diff --git a/guillotina/db/transaction.py b/guillotina/db/transaction.py index 968d3cf92..d74e43144 100644 --- a/guillotina/db/transaction.py +++ b/guillotina/db/transaction.py @@ -3,6 +3,7 @@ from guillotina._settings import app_settings from guillotina.component import get_adapter from guillotina.component import query_adapter +from guillotina.db.interfaces import IReader from guillotina.db.interfaces import ITransaction from guillotina.db.interfaces import ITransactionCache from guillotina.db.interfaces import ITransactionStrategy @@ -318,7 +319,7 @@ async def get(self, oid: str, ignore_registered: bool = False) -> IBaseObject: if result is None: result = await self._get(oid) - obj = app_settings["object_reader"](result) + obj = get_adapter(result, IReader, name=app_settings["db_serializer"]) obj.__txn__ = self if obj.__immutable_cache__: # ttl of zero means we want to provide a hard cache here @@ -392,7 +393,7 @@ async def _store_object(self, obj, uid, added=False): else: serial = getattr(obj, "__serial__", None) or 0 - writer = IWriter(obj) + writer = get_adapter(obj, IWriter, name=app_settings["db_serializer"]) await self._manager._storage.store(uid, serial, writer, obj, self) obj.__serial__ = self._tid obj.__uuid__ = uid @@ -459,7 +460,7 @@ async def get_child(self, parent, key): return self._fill_object(result, parent) def _fill_object(self, item: dict, parent: IBaseObject) -> IBaseObject: - obj = app_settings["object_reader"](item) + obj = get_adapter(item, IReader, name=app_settings["db_serializer"]) obj.__parent__ = parent obj.__txn__ = self return obj @@ -539,7 +540,7 @@ async def get_annotation(self, base_obj, id, reader=None): if result == _EMPTY: raise KeyError(id) if reader is None: - obj = app_settings["object_reader"](result) + obj = get_adapter(result, IReader, name=app_settings["db_serializer"]) else: obj = reader(result) obj.__of__ = base_obj.__uuid__ diff --git a/guillotina/factory/app.py b/guillotina/factory/app.py index df6a894ce..c817496f1 100644 --- a/guillotina/factory/app.py +++ b/guillotina/factory/app.py @@ -139,7 +139,6 @@ def load_application(module, root, settings): "cors_renderer", "check_writable_request", "indexer", - "object_reader", ) _moved = {"oid_generator": "uid_generator", "request_indexer": "indexer"} @@ -233,7 +232,7 @@ async def startup_app(config_file=None, settings=None, loop=None, server_app=Non configure.scan("guillotina.db.strategies") configure.scan("guillotina.db.storages.vacuum") configure.scan("guillotina.db.cache") - configure.scan("guillotina.db.writer") + configure.scan("guillotina.db.serializers") configure.scan("guillotina.db.factory") configure.scan("guillotina.exc_resp") configure.scan("guillotina.fields") diff --git a/guillotina/factory/content.py b/guillotina/factory/content.py index 290611192..d018d8ab2 100644 --- a/guillotina/factory/content.py +++ b/guillotina/factory/content.py @@ -8,6 +8,7 @@ from guillotina.component import provide_utility from guillotina.const import ROOT_ID from guillotina.db.interfaces import IDatabaseManager +from guillotina.db.interfaces import IReader from guillotina.db.interfaces import ITransaction from guillotina.db.interfaces import ITransactionManager from guillotina.db.interfaces import IWriter @@ -175,16 +176,18 @@ async def initialize(self): await txn._strategy.retrieve_tid() root = await tm._storage.load(txn, ROOT_ID) if root is not None: - root = app_settings["object_reader"](root) + root = get_adapter(root, IReader, name=app_settings["db_serializer"]) root.__txn__ = txn if root.__db_id__ is None: root.__db_id__ = self.__db_id__ - await tm._storage.store(ROOT_ID, 0, IWriter(root), root, txn) + writer = get_adapter(root, IWriter, name=app_settings["db_serializer"]) + await tm._storage.store(ROOT_ID, 0, writer, root, txn) except KeyError: from guillotina.db.db import Root root = Root(self.__db_id__) - await tm._storage.store(ROOT_ID, 0, IWriter(root), root, txn) + writer = get_adapter(root, IWriter, name=app_settings["db_serializer"]) + await tm._storage.store(ROOT_ID, 0, writer, root, txn) finally: await tm.commit(txn=txn) diff --git a/guillotina/testing.py b/guillotina/testing.py index 6aecbfb64..4bc5e6133 100644 --- a/guillotina/testing.py +++ b/guillotina/testing.py @@ -8,6 +8,7 @@ import os +DB_SERIALIZER = os.environ.get("DB_SERIALIZER", "pickle") TESTING_PORT = 55001 TESTING_SETTINGS: Dict[str, Any] = { @@ -70,6 +71,7 @@ def configure_with(func): def get_settings(): settings = deepcopy(TESTING_SETTINGS) + settings["db_serializer"] = DB_SERIALIZER for func in _configurators: lazy_apply(func, settings, _configurators) return settings diff --git a/guillotina/tests/mocks.py b/guillotina/tests/mocks.py index 52d4e753c..ea2307570 100644 --- a/guillotina/tests/mocks.py +++ b/guillotina/tests/mocks.py @@ -1,6 +1,7 @@ from collections import OrderedDict from guillotina import app_settings from guillotina import task_vars +from guillotina.component import get_adapter from guillotina.component import query_adapter from guillotina.db.cache.dummy import DummyCache from guillotina.db.interfaces import IStorage @@ -131,7 +132,7 @@ async def get_child(self, txn, container_uid, key): return self._objects[oid] def store(self, oid, old_serial, writer, ob, txn): - writer = IWriter(ob) + writer = get_adapter(ob, IWriter, name=app_settings["db_serializer"]) self._objects[ob.__uuid__] = { "state": writer.serialize(), "zoid": ob.__uuid__, diff --git a/guillotina/tests/test_commands.py b/guillotina/tests/test_commands.py index ca24d6098..f044cdde0 100644 --- a/guillotina/tests/test_commands.py +++ b/guillotina/tests/test_commands.py @@ -15,6 +15,7 @@ DATABASE = os.environ.get("DATABASE", "DUMMY") DB_SCHEMA = os.environ.get("DB_SCHEMA", "public") +DB_SERIALIZER = os.environ.get("DB_SERIALIZER", "pickle") def test_run_command(command_arguments): @@ -38,6 +39,7 @@ async def run(app): @pytest.mark.skipif(DATABASE != "postgres", reason="Only works with pg") +@pytest.mark.skipif(DB_SERIALIZER != "pickle", reason="DB hardcoded in table.sql with pickle") @pytest.mark.skipif(DB_SCHEMA != "public", reason="Fixture 'container_command' does not support 'db_schema'") def test_run_command_with_container(command_arguments, container_command): _, filepath = mkstemp(suffix=".py") @@ -59,6 +61,7 @@ async def run(container): @pytest.mark.skipif(DATABASE != "postgres", reason="Only works with pg") +@pytest.mark.skipif(DB_SERIALIZER != "pickle", reason="DB hardcoded in table.sql with pickle") @pytest.mark.skipif(DB_SCHEMA != "public", reason="Fixture 'container_command' does not support 'db_schema'") def test_run_vacuum_with_container(command_arguments, container_command): command = VacuumCommand(command_arguments) @@ -66,6 +69,7 @@ def test_run_vacuum_with_container(command_arguments, container_command): @pytest.mark.skipif(DATABASE != "postgres", reason="Only works with pg") +@pytest.mark.skipif(DB_SERIALIZER != "pickle", reason="DB hardcoded in table.sql with pickle") @pytest.mark.skipif(DB_SCHEMA != "public", reason="Fixture 'container_command' does not support 'db_schema'") def test_run_migration(command_arguments, container_command): command = MigrateCommand(command_arguments) diff --git a/guillotina/utils/content.py b/guillotina/utils/content.py index 556711059..ae6cac0cb 100644 --- a/guillotina/utils/content.py +++ b/guillotina/utils/content.py @@ -8,6 +8,7 @@ from guillotina.component import query_multi_adapter from guillotina.const import TRASHED_ID from guillotina.db.interfaces import IDatabaseManager +from guillotina.db.interfaces import IReader from guillotina.db.orm.interfaces import IBaseObject from guillotina.exceptions import DatabaseNotFound from guillotina.interfaces import IAbsoluteURL @@ -183,7 +184,7 @@ async def get_object_by_uid(uid: str, txn=None) -> IBaseObject: if result["parent_id"] == TRASHED_ID: raise KeyError(uid) - obj = app_settings["object_reader"](result) + obj = get_adapter(result, IReader, name=app_settings["db_serializer"]) obj.__txn__ = txn if result["parent_id"]: parent = await get_object_by_uid(result["parent_id"], txn) diff --git a/requirements.txt b/requirements.txt index 0eb751710..8012a9888 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ PyJWT==1.6.0 python-dateutil==2.6.1 PyYaml>=5.1 six==1.11.0 -orjson==2.6.0 +orjson==3.4.4 zope.interface==5.1.0 uvicorn==0.10.8 argon2-cffi==18.3.0