diff --git a/README.md b/README.md index 582f2f5..0ae02b4 100644 --- a/README.md +++ b/README.md @@ -451,6 +451,16 @@ async def aggregations(): # to read more about aggregated functions # visit: https://collerek.github.io/ormar/queries/aggregations/ + +async def with_connect(function): + # note that for any other backend than sqlite you actually need to + # connect to the database to perform db operations + async with database: + await function() + + # note that if you use framework like `fastapi` you shouldn't connect + # in your endpoints but have a global connection pool + # check https://collerek.github.io/ormar/fastapi/ and section with db connection # gather and execute all functions # note - normally import should be at the beginning of the file @@ -462,7 +472,7 @@ for func in [create, read, update, delete, joins, filter_and_sort, subset_of_columns, pagination, aggregations]: print(f"Executing: {func.__name__}") - asyncio.run(func()) + asyncio.run(with_connect(func)) # drop the database tables metadata.drop_all(engine) @@ -521,6 +531,7 @@ Available Model Fields (with required args - optional ones in docs): * `BigInteger()` * `Decimal(scale, precision)` * `UUID()` +* `LargeBinary(max_length)` * `EnumField` - by passing `choices` to any other Field type * `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` * `ForeignKey(to)` diff --git a/docs/fields/field-types.md b/docs/fields/field-types.md index 8a5d6a8..d11622b 100644 --- a/docs/fields/field-types.md +++ b/docs/fields/field-types.md @@ -127,6 +127,17 @@ You can use either `length` and `precision` parameters or `max_digits` and `deci * Sqlalchemy column: `sqlalchemy.JSON` * Type (used for pydantic): `pydantic.Json` +### LargeBinary + +`LargeBinary(max_length)` has a required `max_length` parameter. + +* Sqlalchemy column: `sqlalchemy.LargeBinary` +* Type (used for pydantic): `bytes` + +LargeBinary length is used in some backend (i.e. mysql) to determine the size of the field, +in other backends it's simply ignored yet in ormar it's always required. It should be max +size of the file/bytes in bytes. + ### UUID `UUID(uuid_format: str = 'hex')` has no required parameters. diff --git a/docs/index.md b/docs/index.md index 582f2f5..0ae02b4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -451,6 +451,16 @@ async def aggregations(): # to read more about aggregated functions # visit: https://collerek.github.io/ormar/queries/aggregations/ + +async def with_connect(function): + # note that for any other backend than sqlite you actually need to + # connect to the database to perform db operations + async with database: + await function() + + # note that if you use framework like `fastapi` you shouldn't connect + # in your endpoints but have a global connection pool + # check https://collerek.github.io/ormar/fastapi/ and section with db connection # gather and execute all functions # note - normally import should be at the beginning of the file @@ -462,7 +472,7 @@ for func in [create, read, update, delete, joins, filter_and_sort, subset_of_columns, pagination, aggregations]: print(f"Executing: {func.__name__}") - asyncio.run(func()) + asyncio.run(with_connect(func)) # drop the database tables metadata.drop_all(engine) @@ -521,6 +531,7 @@ Available Model Fields (with required args - optional ones in docs): * `BigInteger()` * `Decimal(scale, precision)` * `UUID()` +* `LargeBinary(max_length)` * `EnumField` - by passing `choices` to any other Field type * `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend` * `ForeignKey(to)` diff --git a/docs/releases.md b/docs/releases.md index 192c932..bc6a148 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,3 +1,14 @@ +# 0.10.6 + +## ✨ Features + +* Add `LargeBinary(max_length)` field type [#166](https://github.com/collerek/ormar/issues/166) + +## 💬 Other + +* Add connecting the database in quickstart in readme [#180](https://github.com/collerek/ormar/issues/180) + + # 0.10.5 ## 🐛 Fixes diff --git a/examples/db.sqlite b/examples/db.sqlite index e9f6c42..95eab2d 100644 Binary files a/examples/db.sqlite and b/examples/db.sqlite differ diff --git a/examples/script_from_readme.py b/examples/script_from_readme.py index ce4ee92..782841b 100644 --- a/examples/script_from_readme.py +++ b/examples/script_from_readme.py @@ -319,6 +319,16 @@ async def aggregations(): # visit: https://collerek.github.io/ormar/queries/aggregations/ +async def with_connect(function): + # note that for any other backend than sqlite you actually need to + # connect to the database to perform db operations + async with database: + await function() + + # note that if you use framework like `fastapi` you shouldn't connect + # in your endpoints but have a global connection pool + # check https://collerek.github.io/ormar/fastapi/ and section with db connection + # gather and execute all functions # note - normally import should be at the beginning of the file import asyncio @@ -329,7 +339,7 @@ for func in [create, read, update, delete, joins, filter_and_sort, subset_of_columns, pagination, aggregations]: print(f"Executing: {func.__name__}") - asyncio.run(func()) + asyncio.run(with_connect(func)) # drop the database tables metadata.drop_all(engine) \ No newline at end of file diff --git a/ormar/__init__.py b/ormar/__init__.py index 8b4f371..1c3d104 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -53,6 +53,7 @@ from ormar.fields import ( ForeignKeyField, Integer, JSON, + LargeBinary, ManyToMany, ManyToManyField, String, @@ -124,4 +125,5 @@ __all__ = [ "EncryptBackends", "ENCODERS_MAP", "DECODERS_MAP", + "LargeBinary", ] diff --git a/ormar/fields/__init__.py b/ormar/fields/__init__.py index e0cb3b0..bc679c4 100644 --- a/ormar/fields/__init__.py +++ b/ormar/fields/__init__.py @@ -16,6 +16,7 @@ from ormar.fields.model_fields import ( Float, Integer, JSON, + LargeBinary, String, Text, Time, @@ -50,4 +51,5 @@ __all__ = [ "EncryptBackend", "DECODERS_MAP", "ENCODERS_MAP", + "LargeBinary", ] diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index 2236e71..3f4417c 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -415,6 +415,53 @@ class JSON(ModelFieldFactory, pydantic.Json): return sqlalchemy.JSON() +class LargeBinary(ModelFieldFactory, bytes): + """ + LargeBinary field factory that construct Field classes and populated their values. + """ + + _type = bytes + + def __new__( # type: ignore # noqa CFQ002 + cls, *, max_length: int = None, **kwargs: Any + ) -> BaseField: # type: ignore + kwargs = { + **kwargs, + **{ + k: v + for k, v in locals().items() + if k not in ["cls", "__class__", "kwargs"] + }, + } + return super().__new__(cls, **kwargs) + + @classmethod + def get_column_type(cls, **kwargs: Any) -> Any: + """ + Return proper type of db column for given field type. + Accepts required and optional parameters that each column type accepts. + + :param kwargs: key, value pairs of sqlalchemy options + :type kwargs: Any + :return: initialized column with proper options + :rtype: sqlalchemy Column + """ + return sqlalchemy.LargeBinary(length=kwargs.get("max_length")) + + @classmethod + def validate(cls, **kwargs: Any) -> None: + """ + Used to validate if all required parameters on a given field type are set. + :param kwargs: all params passed during construction + :type kwargs: Any + """ + max_length = kwargs.get("max_length", None) + if max_length is None or max_length <= 0: + raise ModelDefinitionError( + "Parameter max_length is required for field LargeBinary" + ) + + class BigInteger(Integer, int): """ BigInteger field factory that construct Field classes and populated their values. diff --git a/ormar/models/helpers/validation.py b/ormar/models/helpers/validation.py index a535f65..e45f3a8 100644 --- a/ormar/models/helpers/validation.py +++ b/ormar/models/helpers/validation.py @@ -73,6 +73,8 @@ def convert_choices_if_needed( # noqa: CCR001 else value ) choices = [round(float(o), precision) for o in choices] + elif field.__type__ == bytes: + value = value if isinstance(value, bytes) else value.encode("utf-8") return value, choices diff --git a/tests/test_fastapi/test_choices_schema.py b/tests/test_fastapi/test_choices_schema.py index 978c3f6..6d87fa4 100644 --- a/tests/test_fastapi/test_choices_schema.py +++ b/tests/test_fastapi/test_choices_schema.py @@ -1,6 +1,7 @@ import datetime import decimal import uuid +from base64 import b64encode from enum import Enum import databases @@ -22,6 +23,10 @@ uuid1 = uuid.uuid4() uuid2 = uuid.uuid4() +blob = b"test" +blob2 = b"test2icac89uc98" + + class EnumTest(Enum): val1 = "Val1" val2 = "Val2" @@ -57,6 +62,7 @@ class Organisation(ormar.Model): random_json: pydantic.Json = ormar.JSON(choices=["aa", '{"aa":"bb"}']) random_uuid: uuid.UUID = ormar.UUID(choices=[uuid1, uuid2]) enum_string: str = ormar.String(max_length=100, choices=list(EnumTest)) + blob_col: bytes = ormar.LargeBinary(max_length=100000, choices=[blob, blob2]) @app.on_event("startup") @@ -111,6 +117,7 @@ def test_all_endpoints(): "random_json": '{"aa":"bb"}', "random_uuid": str(uuid1), "enum_string": EnumTest.val1.value, + "blob_col": blob.decode("utf-8"), }, ) diff --git a/tests/test_model_definition/test_models.py b/tests/test_model_definition/test_models.py index 3459768..46d8ba0 100644 --- a/tests/test_model_definition/test_models.py +++ b/tests/test_model_definition/test_models.py @@ -1,6 +1,6 @@ import asyncio -import uuid import datetime +import uuid from typing import List import databases @@ -9,7 +9,7 @@ import pytest import sqlalchemy import ormar -from ormar.exceptions import QueryDefinitionError, NoMatch, ModelError +from ormar.exceptions import ModelError, NoMatch, QueryDefinitionError from tests.settings import DATABASE_URL database = databases.Database(DATABASE_URL, force_rollback=True) @@ -26,6 +26,20 @@ class JsonSample(ormar.Model): test_json = ormar.JSON(nullable=True) +blob = b"test" +blob2 = b"test2icac89uc98" + + +class LargeBinarySample(ormar.Model): + class Meta: + tablename = "my_bolbs" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + test_binary = ormar.LargeBinary(max_length=100000, choices=[blob, blob2]) + + class UUIDSample(ormar.Model): class Meta: tablename = "uuids" @@ -102,15 +116,8 @@ class Country(ormar.Model): ) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - loop.close() - - @pytest.fixture(autouse=True, scope="module") -async def create_test_database(): +def create_test_database(): engine = sqlalchemy.create_engine(DATABASE_URL) metadata.drop_all(engine) metadata.create_all(engine) @@ -151,6 +158,19 @@ async def test_json_column(): assert items[1].test_json == dict(aa=12) +@pytest.mark.asyncio +async def test_binary_column(): + async with database: + async with database.transaction(force_rollback=True): + await LargeBinarySample.objects.create(test_binary=blob) + await LargeBinarySample.objects.create(test_binary=blob2) + + items = await LargeBinarySample.objects.all() + assert len(items) == 2 + assert items[0].test_binary == blob + assert items[1].test_binary == blob2 + + @pytest.mark.asyncio async def test_uuid_column(): async with database: diff --git a/tests/test_types.py b/tests/test_types.py index ba819ed..d4f8dad 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -57,7 +57,7 @@ def create_test_database(): def assert_type(book: Book): - print(book) + _ = str(book) @pytest.mark.asyncio