diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 966ed1e..f461e50 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,5 +33,9 @@ jobs: poetry config virtualenvs.in-project true poetry run pip install -U pip poetry install + - name: Pull up db + run: make db-up - name: Run tests run: make test + - name: Pull down db + run: make db-down diff --git a/Makefile b/Makefile index 724e4ab..27569da 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,37 @@ SHELL:=/usr/bin/env bash +.PHONY: db-up +db-up: + docker compose up -d + +.PHONY: db-down +db-down: + docker compose down -v + .PHONY: unit unit: poetry run pytest +.PHONY: benchmark +benchmark: + poetry run python3 benchmarks/jsonfield_benchmark.py + .PHONY: typing typing: poetry run mypy src .PHONY: lint lint: - poetry run ruff check --select I - poetry run ruff format --check + poetry run ruff check --select I src + poetry run ruff format --check src .PHONY: format format: - poetry run ruff check --select I --fix - poetry run ruff format + poetry run ruff check --select I --fix src + poetry run ruff format src .PHONY: test -test: unit \ No newline at end of file +test: unit + +.PHONY: all-checks +all-checks: lint typing test \ No newline at end of file diff --git a/README.md b/README.md index 4038e26..c799bc8 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,18 @@ > ⚠️ This project under active developing. -Django extension for efficient and easy storing `dict` data in django field. +Django extension for storing dict data in relation database field with fast performance and flex schema. ## 📍 Purpose -Sometimes we need to store some key value data in storages. In Django we can use `JSONField` with `json` tool to convert or `BinaryField` with some customs transformations. Often it also need to be efficient for work with big data or to have some validation and another features. +Sometimes we need to store some key value data in storages. Often it also need to be efficient for work with big data and have some validation and another features. + +`django-dict-field` is a tool build around amazing [msgspec](https://github.com/jcrist/msgspec) serialization and validation library for solving this problems like a charm ✨ + +**DictField vs JSONField Benchmark** + +![DictField vs JSONField Benchmark](docs/media/bechmark.png) -`django-dict-field` here is to solve this problems like a charm ✨ ## 🚀 Quick start diff --git a/tests/django_app/params_field/__init__.py b/benchmarks/__init__.py similarity index 100% rename from tests/django_app/params_field/__init__.py rename to benchmarks/__init__.py diff --git a/benchmarks/jsonfield_benchmark.py b/benchmarks/jsonfield_benchmark.py new file mode 100644 index 0000000..c2c46cc --- /dev/null +++ b/benchmarks/jsonfield_benchmark.py @@ -0,0 +1,105 @@ +import os +import random +import sys +import timeit + +import django +from django.db import connection +from django.test.utils import setup_test_environment, teardown_test_environment +from tabulate import tabulate + +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +django_app_path = os.path.join(project_root, "tests", "django_app") + +sys.path.insert(0, project_root) +sys.path.insert(0, django_app_path) + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_app.settings") +django.setup() + +setup_test_environment() +db_name = connection.creation.create_test_db(verbosity=1, autoclobber=True) + +from dict_field.models import ModelForTest + +DICT_SIZE = 100_000 +BIG_DICT = {str(i): i for i in range(DICT_SIZE)} + + +def get_big_dict_field(test_model: ModelForTest, field_name: str) -> None: + rand_index = str(random.randint(0, DICT_SIZE - 1)) + test_model = ModelForTest.objects.get(pk=test_model.pk) + test_model.__getattribute__(field_name)[rand_index] + + +def delete_big_dict_field(test_model: ModelForTest, field_name: str) -> None: + rand_index = str(random.randint(0, DICT_SIZE - 1)) + val = test_model.__getattribute__(field_name)[rand_index] + del test_model.__getattribute__(field_name)[rand_index] + test_model.save() + # NOTE: Here is overhead because of saving deleted value. Must to figure + # out how to test delete correctly. + test_model.__getattribute__(field_name)[rand_index] = val + test_model.save() + + +def update_big_dict_field(test_model: ModelForTest, field_name: str) -> None: + rand_index = str(random.randint(0, DICT_SIZE - 1)) + test_model.__getattribute__(field_name)[rand_index] = "restored" + test_model.save() + + +def run_bench(): + # NOTE: Can take a while. Be careful. + number = 1000 + test_model_jsonfield = ModelForTest.objects.create( + default_json_field=BIG_DICT, default_dict_field={} + ) + test_model_dictfield = ModelForTest.objects.create( + default_json_field={}, default_dict_field=BIG_DICT + ) + + # JSONField + time_to_get = timeit.timeit( + lambda: get_big_dict_field(test_model_jsonfield, "default_json_field"), + number=number, + ) + time_to_delete = timeit.timeit( + lambda: delete_big_dict_field(test_model_jsonfield, "default_json_field"), + number=number, + ) + time_to_update = timeit.timeit( + lambda: update_big_dict_field(test_model_jsonfield, "default_json_field"), + number=number, + ) + json_field_time = ("JSONField", time_to_get, time_to_update, time_to_delete) + + # DictField + time_to_get = timeit.timeit( + lambda: get_big_dict_field(test_model_dictfield, "default_dict_field"), + number=number, + ) + time_to_delete = timeit.timeit( + lambda: delete_big_dict_field(test_model_dictfield, "default_dict_field"), + number=number, + ) + time_to_update = timeit.timeit( + lambda: update_big_dict_field(test_model_dictfield, "default_dict_field"), + number=number, + ) + dict_field_time = ("DictField", time_to_get, time_to_update, time_to_delete) + + report_table = tabulate( + (json_field_time, dict_field_time), + headers=("Field", "Get", "Delete", "Update"), + tablefmt="grid", + ) + print(report_table) + + +if __name__ == "__main__": + try: + run_bench() + finally: + teardown_test_environment() + connection.creation.destroy_test_db(db_name, verbosity=1) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dda60d1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + postgres: + image: postgres:15.14-alpine3.21 + ports: + - "8888:5432" + environment: + POSTGRES_PASSWORD: postgrespassword \ No newline at end of file diff --git a/docs/media/bechmark.png b/docs/media/bechmark.png new file mode 100644 index 0000000..772398f Binary files /dev/null and b/docs/media/bechmark.png differ diff --git a/poetry.lock b/poetry.lock index 5a6ec94..b50595b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -225,6 +225,77 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "msgspec" +version = "0.20.0" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "msgspec-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23a6ec2a3b5038c233b04740a545856a068bc5cb8db184ff493a58e08c994fbf"}, + {file = "msgspec-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cde2c41ed3eaaef6146365cb0d69580078a19f974c6cb8165cc5dcd5734f573e"}, + {file = "msgspec-0.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5da0daa782f95d364f0d95962faed01e218732aa1aa6cad56b25a5d2092e75a4"}, + {file = "msgspec-0.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9369d5266144bef91be2940a3821e03e51a93c9080fde3ef72728c3f0a3a8bb7"}, + {file = "msgspec-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90fb865b306ca92c03964a5f3d0cd9eb1adda14f7e5ac7943efd159719ea9f10"}, + {file = "msgspec-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8112cd48b67dfc0cfa49fc812b6ce7eb37499e1d95b9575061683f3428975d3"}, + {file = "msgspec-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:666b966d503df5dc27287675f525a56b6e66a2b8e8ccd2877b0c01328f19ae6c"}, + {file = "msgspec-0.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:099e3e85cd5b238f2669621be65f0728169b8c7cb7ab07f6137b02dc7feea781"}, + {file = "msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520"}, + {file = "msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54"}, + {file = "msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854"}, + {file = "msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53"}, + {file = "msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e"}, + {file = "msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a"}, + {file = "msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29"}, + {file = "msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520"}, + {file = "msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5"}, + {file = "msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde"}, + {file = "msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054"}, + {file = "msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0"}, + {file = "msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870"}, + {file = "msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e"}, + {file = "msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb"}, + {file = "msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7"}, + {file = "msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46"}, + {file = "msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8"}, + {file = "msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee"}, + {file = "msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111"}, + {file = "msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f"}, + {file = "msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c"}, + {file = "msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352"}, + {file = "msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458"}, + {file = "msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a"}, + {file = "msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238"}, + {file = "msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42"}, + {file = "msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0"}, + {file = "msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae"}, + {file = "msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980"}, + {file = "msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b"}, + {file = "msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0"}, + {file = "msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5"}, + {file = "msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131"}, + {file = "msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56"}, + {file = "msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846"}, + {file = "msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63"}, + {file = "msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8"}, + {file = "msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d"}, + {file = "msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc"}, + {file = "msgspec-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eee56472ced14602245ac47516e179d08c6c892d944228796f239e983de7449c"}, + {file = "msgspec-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19395e9a08cc5bd0e336909b3e13b4ae5ee5e47b82e98f8b7801d5a13806bb6f"}, + {file = "msgspec-0.20.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d5bb7ce84fe32f6ce9f62aa7e7109cb230ad542cc5bc9c46e587f1dac4afc48e"}, + {file = "msgspec-0.20.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c6da9ae2d76d11181fbb0ea598f6e1d558ef597d07ec46d689d17f68133769f"}, + {file = "msgspec-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84d88bd27d906c471a5ca232028671db734111996ed1160e37171a8d1f07a599"}, + {file = "msgspec-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:03907bf733f94092a6b4c5285b274f79947cad330bd8a9d8b45c0369e1a3c7f0"}, + {file = "msgspec-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:9fbcb660632a2f5c247c0dc820212bf3a423357ac6241ff6dc6cfc6f72584016"}, + {file = "msgspec-0.20.0-cp39-cp39-win_arm64.whl", hash = "sha256:f7cd0e89b86a16005745cb99bd1858e8050fc17f63de571504492b267bca188a"}, + {file = "msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29"}, +] + +[package.extras] +toml = ["tomli ; python_version < \"3.11\"", "tomli_w"] +yaml = ["pyyaml"] + [[package]] name = "mypy" version = "1.15.0" @@ -319,6 +390,83 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + [[package]] name = "pytest" version = "8.3.5" @@ -342,6 +490,27 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-benchmark" +version = "5.1.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105"}, + {file = "pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=8.1" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs", "setuptools"] + [[package]] name = "pytest-cov" version = "6.1.1" @@ -445,6 +614,21 @@ files = [ dev = ["build", "hatch"] doc = ["sphinx"] +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + [[package]] name = "tomli" version = "2.2.1" @@ -530,4 +714,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "c34a39280e7b88ab72f7f3f9838eb82c981d7ebe6863630093d2410559182e73" +content-hash = "b8ce6443343e6d0e6289f457fe49a97b179be70705e96548b27696dddd3ce5fa" diff --git a/pyproject.toml b/pyproject.toml index 42011e4..0e4c067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] -name = "django-params-field" +name = "django-dict-field" version = "0.0.1" -description = "Django extension for storing a lot of parameters in one field." +description = "Django extension for efficient and easy storing dict data in django field." authors = [{ name = "skv0zsneg", email = "" }] readme = "README.md" requires-python = ">=3.9" -dependencies = ["django>=4.2.0"] +dependencies = ["django>=4.2.0", "msgspec (>=0.20.0,<0.21.0)"] [tool.poetry.group.dev.dependencies] pytest = "^8.3.5" @@ -16,6 +16,9 @@ pytest-cov = "^6.1.1" pytest-xdist = "^3.7.0" django-stubs = { extras = ["compatible-mypy"], version = "^5.2.0", markers = "python_version >= '3.10'" } ruff = "^0.11.12" +pytest-benchmark = "^5.1.0" +tabulate = "^0.9.0" +psycopg2-binary = "^2.9.11" [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "django_app.settings" diff --git a/src/django_dict_field/__init__.py b/src/django_dict_field/__init__.py new file mode 100644 index 0000000..1331120 --- /dev/null +++ b/src/django_dict_field/__init__.py @@ -0,0 +1,3 @@ +from .dict_field import DictField + +__all__ = ("DictField",) diff --git a/src/django_dict_field/dict_field.py b/src/django_dict_field/dict_field.py new file mode 100644 index 0000000..9627840 --- /dev/null +++ b/src/django_dict_field/dict_field.py @@ -0,0 +1,35 @@ +from typing import Optional + +from django.core.exceptions import ValidationError +from django.db.models.fields import BinaryField + +from django_dict_field.serializer import Serializer + + +class DictField(BinaryField): + """Storing a set of params in one field.""" + + def __init__(self, *args, **kwargs) -> None: + self.serializer = Serializer() + kwargs.setdefault("editable", False) + super().__init__(*args, **kwargs) + + def from_db_value(self, value: Optional[bytes], *args, **kwargs) -> Optional[dict]: + if value is None: + return value + return self.serializer.deserialize(value) + + def to_python(self, value: dict) -> dict: + if not isinstance(value, dict): + raise ValidationError(f"Given value '{value}' must be 'dict' instance!") + return value + + def get_db_prep_value( + self, value: Optional[dict], *args, **kwargs + ) -> Optional[bytes]: + if value is None: + return value + return self.serializer.serialize(value) + + def value_to_string(self, obj): + return str(self.value_from_object(obj)) diff --git a/src/django_dict_field/serializer.py b/src/django_dict_field/serializer.py new file mode 100644 index 0000000..de2d7bb --- /dev/null +++ b/src/django_dict_field/serializer.py @@ -0,0 +1,11 @@ +import msgspec + + +class Serializer: + """Params serializer & deserializer.""" + + def serialize(self, value: dict) -> bytes: + return msgspec.json.encode(value) + + def deserialize(self, value: bytes) -> dict: + return msgspec.json.decode(value) diff --git a/src/django_params_field/__init__.py b/src/django_params_field/__init__.py deleted file mode 100644 index a73bbaf..0000000 --- a/src/django_params_field/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .params_field import ParamsField - -__all__ = ("ParamsField",) diff --git a/src/django_params_field/params_field.py b/src/django_params_field/params_field.py deleted file mode 100644 index 205713b..0000000 --- a/src/django_params_field/params_field.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Any, Generic, Optional - -from django.core.exceptions import ValidationError -from django.db.models.fields import BinaryField - -from django_params_field.serializer import P, Serializer - - -class ParamsField(BinaryField, Generic[P]): - """Storing a set of params in one field.""" - - def __init__(self, params_type: Optional[P] = None, *args, **kwargs) -> None: - self.params_type = params_type - self.serializer = Serializer(self.params_type) - super().__init__(*args, **kwargs) - - def deconstruct(self) -> tuple[Any, Any, Any, Any]: - name, path, args, kwargs = super().deconstruct() - kwargs["params_type"] = self.params_type - return name, path, args, kwargs - - def from_db_value(self, value: bytes, *args, **kwargs) -> P: - return self.serializer.deserialize(value) - - def to_python(self, value: P) -> P: - return value - - def get_db_prep_value(self, value: P, *args, **kwargs) -> bytes: - return self.serializer.serialize(value) - - def validate(self, value: P, *args, **kwargs) -> None: - if not self.serializer.is_valid(value): - raise ValidationError(f"Given value '{value}' is not valid!") diff --git a/src/django_params_field/serializer.py b/src/django_params_field/serializer.py deleted file mode 100644 index 5ef4751..0000000 --- a/src/django_params_field/serializer.py +++ /dev/null @@ -1,31 +0,0 @@ -import bz2 -import pickle -from typing import Any, ClassVar, Generic, Optional, TypeVar - -P = TypeVar("P") - - -class Serializer(Generic[P]): - """Params serializer & deserializer.""" - - ALLOWED_TYPES: ClassVar[tuple[Any, ...]] = (dict,) - - def __init__(self, params_type: Optional[P] = None) -> None: - self.params_type = params_type - - def serialize(self, value: P) -> bytes: - serialized = pickle.dumps(value) - compressed = bz2.compress(serialized) - return compressed - - def deserialize(self, value: bytes) -> P: - decompressed = bz2.decompress(value) - deserialized = pickle.loads(decompressed) - return deserialized - - def is_valid(self, value: P) -> bool: - return any( - map( - lambda allowed_type: isinstance(value, allowed_type), self.ALLOWED_TYPES - ) - ) diff --git a/tests/django_app/params_field/migrations/__init__.py b/tests/django_app/dict_field/__init__.py similarity index 100% rename from tests/django_app/params_field/migrations/__init__.py rename to tests/django_app/dict_field/__init__.py diff --git a/tests/django_app/params_field/apps.py b/tests/django_app/dict_field/apps.py similarity index 60% rename from tests/django_app/params_field/apps.py rename to tests/django_app/dict_field/apps.py index be92398..98da121 100644 --- a/tests/django_app/params_field/apps.py +++ b/tests/django_app/dict_field/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class ParamsFieldConfig(AppConfig): +class DictFieldConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "params_field" + name = "dict_field" diff --git a/tests/django_app/params_field/migrations/0001_initial.py b/tests/django_app/dict_field/migrations/0001_initial.py similarity index 68% rename from tests/django_app/params_field/migrations/0001_initial.py rename to tests/django_app/dict_field/migrations/0001_initial.py index 181b02d..2da9b47 100644 --- a/tests/django_app/params_field/migrations/0001_initial.py +++ b/tests/django_app/dict_field/migrations/0001_initial.py @@ -1,11 +1,12 @@ -# Generated by Django 5.2.1 on 2025-05-30 20:21 +# Generated by Django 4.2.22 on 2025-09-20 19:12 from django.db import migrations, models -import django_params_field.params_field +import django_dict_field.dict_field class Migration(migrations.Migration): + initial = True dependencies = [] @@ -23,10 +24,8 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ( - "params_field_default", - django_params_field.params_field.ParamsField(params_type=None), - ), + ("default_json_field", models.JSONField(null=True)), + ("default_dict_field", django_dict_field.dict_field.DictField()), ], ), ] diff --git a/tests/django_app/dict_field/migrations/__init__.py b/tests/django_app/dict_field/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/django_app/dict_field/models.py b/tests/django_app/dict_field/models.py new file mode 100644 index 0000000..0e2a0e8 --- /dev/null +++ b/tests/django_app/dict_field/models.py @@ -0,0 +1,9 @@ +from django.db import models +from django.db.models.fields.json import JSONField + +from django_dict_field import DictField + + +class ModelForTest(models.Model): + default_json_field = JSONField(null=True) + default_dict_field = DictField() diff --git a/tests/django_app/dict_field/tests/__init__.py b/tests/django_app/dict_field/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/django_app/dict_field/tests/test_default_dict_field.py b/tests/django_app/dict_field/tests/test_default_dict_field.py new file mode 100644 index 0000000..fdb28f2 --- /dev/null +++ b/tests/django_app/dict_field/tests/test_default_dict_field.py @@ -0,0 +1,103 @@ +import pytest +from django.core.exceptions import ValidationError +from django.db.utils import IntegrityError + +from ..models import ModelForTest + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "value", + [ + {}, + {"level1": {"level2": {"key": "value"}}}, + { + "string": "text", + "integer": 42, + "float": 3.14, + "boolean": True, + "list": [1, 2, 3], + "null": None, + }, + {"key": "value", "nested": {"a": 1}}, + ], +) +def test_dictfield_good_values(value): + model = ModelForTest.objects.create(default_dict_field=value) + model.refresh_from_db() + assert model.default_dict_field == value + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "invalid_value", + [ + "not a dict", + 123, + 3.14, + ["list"], + (1, 2), + ], +) +def test_dictfield_bad_values(invalid_value): + model = ModelForTest.objects.create(default_dict_field=invalid_value) + with pytest.raises(ValidationError): + model.full_clean() + + +@pytest.mark.django_db +def test_dictfield_None_value(): + with pytest.raises(IntegrityError): + ModelForTest.objects.create(default_dict_field=None) + + +@pytest.mark.django_db +def test_dictfield_modifying_inplace(): + model = ModelForTest.objects.create(default_dict_field={"a": 1}) + model.default_dict_field["b"] = 2 + model.save() + model.refresh_from_db() + assert model.default_dict_field == {"a": 1, "b": 2} + + +@pytest.mark.django_db +def test_dictfield_clearing(): + model = ModelForTest.objects.create(default_dict_field={"a": 1}) + model.default_dict_field.clear() + model.save() + model.refresh_from_db() + assert model.default_dict_field == {} + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "value,expected", + [ + ({"a": 1}, "{'a': 1}"), + ({}, "{}"), + ({"x": [1, 2]}, "{'x': [1, 2]}"), + ], +) +def test_dictfield_value_to_string(value, expected): + model = ModelForTest.objects.create(default_dict_field=value) + field = model._meta.get_field("default_dict_field") + assert field.value_to_string(model) == expected + + +@pytest.mark.django_db +def test_dictfield_from_db_value_none(): + field = ModelForTest._meta.get_field("default_dict_field") + assert field.from_db_value(None, None) is None + + +@pytest.mark.parametrize( + "value", + [ + {}, + {"a": 1}, + {"nested": {"x": 10}}, + ], +) +def test_dictfield_from_db_value_none_to_python_with_valid_dict(value): + field = ModelForTest._meta.get_field("default_dict_field") + assert field.to_python(value) == value diff --git a/tests/django_app/django_app/settings.py b/tests/django_app/django_app/settings.py index f98e9d3..1edccac 100644 --- a/tests/django_app/django_app/settings.py +++ b/tests/django_app/django_app/settings.py @@ -1,35 +1,10 @@ -""" -Django settings for django_app project. - -Generated by 'django-admin startproject' using Django 5.2.1. - -For more information on this file, see -https://docs.djangoproject.com/en/5.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.2/ref/settings/ -""" - from pathlib import Path -# Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-4b=4+et^j4s$0h)b^gf&(j*@mhibjl!dknh0y4dgb(s*tbpd83" - -# SECURITY WARNING: don't run with debug turned on in production! +SECRET_KEY = "keep-it-secret" DEBUG = True - ALLOWED_HOSTS = [] - -# Application definition - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -37,7 +12,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "params_field", + "dict_field", ] MIDDLEWARE = [ @@ -69,21 +44,17 @@ WSGI_APPLICATION = "django_app.wsgi.application" - -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases - DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": "django.db.backends.postgresql", + "NAME": "dbfortests", + "USER": "postgres", + "PASSWORD": "postgrespassword", # from docker-compose.yml + "HOST": "localhost", + "PORT": "8888", # from docker-compose.yml } } - -# Password validation -# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", @@ -100,24 +71,10 @@ ] -# Internationalization -# https://docs.djangoproject.com/en/5.2/topics/i18n/ - LANGUAGE_CODE = "en-us" - TIME_ZONE = "UTC" - USE_I18N = True - USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.2/howto/static-files/ - STATIC_URL = "static/" - -# Default primary key field type -# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field - DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tests/django_app/params_field/models.py b/tests/django_app/params_field/models.py deleted file mode 100644 index a887e4f..0000000 --- a/tests/django_app/params_field/models.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import models - -from django_params_field import ParamsField - - -class ModelForTest(models.Model): - """Simple model using for test ParamsField.""" - - params_field_default = ParamsField() diff --git a/tests/django_app/params_field/tests.py b/tests/django_app/params_field/tests.py deleted file mode 100644 index 1e2fe24..0000000 --- a/tests/django_app/params_field/tests.py +++ /dev/null @@ -1,60 +0,0 @@ -import pytest -from django.core.exceptions import ValidationError - -from .models import ModelForTest - - -@pytest.mark.django_db -def test_model_full_clean_with_correct_value(): - """Test model `clean()` params_field_default.""" - test_model = ModelForTest.objects.create(params_field_default={"just": "test"}) - # check that no exception raised - assert test_model.full_clean() is None - - -@pytest.mark.django_db -def test_model_full_clean_with_bas_value(): - """Test model `clean()` params_field_default.""" - test_model = ModelForTest.objects.create(params_field_default="I'm Not Valid!") - with pytest.raises(ValidationError): - test_model.full_clean() - - -@pytest.mark.django_db -@pytest.mark.parametrize("dict_size", [1000, 10_000, 100_000, 1_000_000]) -def test_set_and_get_params_field_default(dict_size): - """Test set and get params fields different size with default ``params_type``.""" - test_model = ModelForTest.objects.create() - simple_data_struct = {i: i for i in range(dict_size)} - test_model.params_field_default = simple_data_struct - test_model.save() - - test_model_from_query = ModelForTest.objects.get(pk=test_model.pk) - assert test_model_from_query.params_field_default == simple_data_struct - - -@pytest.mark.django_db -@pytest.mark.parametrize("dict_size", [1000, 10_000, 100_000, 1_000_000]) -def test_update_params_field_default(dict_size): - """Test update params fields different size with default ``params_type``.""" - simple_data_struct = {i: i for i in range(dict_size)} - test_model = ModelForTest.objects.create(params_field_default=simple_data_struct) - - test_model.params_field_default[0] = "test-change-value" - test_model.save() - test_model_from_query = ModelForTest.objects.get(pk=test_model.pk) - assert test_model_from_query.params_field_default[0] == "test-change-value" - - -@pytest.mark.django_db -@pytest.mark.parametrize("dict_size", [1000, 10_000, 100_000, 1_000_000]) -def test_delete_params_field_default(dict_size): - """Test delete params fields different size with default ``params_type``.""" - simple_data_struct = {i: i for i in range(dict_size)} - test_model = ModelForTest.objects.create(params_field_default=simple_data_struct) - - keys_num_before = len(test_model.params_field_default.keys()) - del test_model.params_field_default[1] - test_model.save() - test_model_from_query = ModelForTest.objects.get(pk=test_model.pk) - assert len(test_model_from_query.params_field_default.keys()) == keys_num_before - 1