diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index fa38da7..8de8871 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,7 +16,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install mkdocs-material + pip install mkdocs-material pydoc-markdown + - name: Build Api docs + run: pydoc-markdown -- build --site-dir=api + - name: Copy APi docs + run: cp -Tavr ./build/docs/content/ ./docs/api/ - name: Deploy run: | mkdocs gh-deploy --force diff --git a/docs/fields/field-types.md b/docs/fields/field-types.md index 10dcc24..8a5d6a8 100644 --- a/docs/fields/field-types.md +++ b/docs/fields/field-types.md @@ -145,6 +145,49 @@ Sample: When loaded it's always python UUID so you can compare it and compare two formats values between each other. +### Enum + +Although there is no dedicated field type for Enums in `ormar` you can change any +field into `Enum` like field by passing a `choices` list that is accepted by all Field types. + +It will add both: validation in `pydantic` model and will display available options in schema, +therefore it will be available in docs of `fastapi`. + +If you still want to use `Enum` in your application you can do this by passing a `Enum` into choices +and later pass value of given option to a given field (note tha Enum is not JsonSerializable). + +```python +# not that imports and endpoints declaration +# is skipped here for brevity +from enum import Enum +class TestEnum(Enum): + val1 = 'Val1' + val2 = 'Val2' + +class TestModel(ormar.Model): + class Meta: + tablename = "org" + metadata = metadata + database = database + + id: int = ormar.Integer(primary_key=True) + # pass list(Enum) to choices + enum_string: str = ormar.String(max_length=100, choices=list(TestEnum)) + +# sample payload coming to fastapi +response = client.post( + "/test_models/", + json={ + "id": 1, + # you need to refer to the value of the `Enum` option + # if called like this, alternatively just use value + # string "Val1" in this case + "enum_string": TestEnum.val1.value + }, +) + +``` + [relations]: ../relations/index.md [queries]: ../queries.md [pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types diff --git a/docs/index.md b/docs/index.md index 2a94149..d871c06 100644 --- a/docs/index.md +++ b/docs/index.md @@ -187,6 +187,7 @@ Available Model Fields (with required args - optional ones in docs): * `BigInteger()` * `Decimal(scale, precision)` * `UUID()` +* `EnumField` - by passing `choices` to any other Field type * `ForeignKey(to)` * `ManyToMany(to, through)` diff --git a/docs/releases.md b/docs/releases.md index 457a048..c1a07bf 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,3 +1,11 @@ +# 0.9.1 + +## Features +* Add choices values to `OpenAPI` specs, so it looks like native `Enum` field in the result schema. + +## Fixes +* Fix `choices` behavior with `fastapi` usage when special fields can be not initialized yet but passed as strings etc. + # 0.9.0 ## Important diff --git a/ormar/__init__.py b/ormar/__init__.py index 0c84957..7904111 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -65,7 +65,7 @@ class UndefinedType: # pragma no cover Undefined = UndefinedType() -__version__ = "0.9.0" +__version__ = "0.9.1" __all__ = [ "Integer", "BigInteger", diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 6af4c57..3ce2699 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -1,6 +1,7 @@ import datetime import decimal import uuid +from enum import Enum from typing import ( Any, Dict, @@ -88,13 +89,19 @@ def check_if_field_has_choices(field: Type[BaseField]) -> bool: return hasattr(field, "choices") and bool(field.choices) -def convert_choices_if_needed( +def convert_choices_if_needed( # noqa: CCR001 field: Type["BaseField"], values: Dict ) -> Tuple[Any, List]: """ Converts dates to isoformat as fastapi can check this condition in routes and the fields are not yet parsed. + Converts enums to list of it's values. + + Converts uuids to strings. + + Converts decimal to float with given scale. + :param field: ormar field to check with choices :type field: Type[BaseField] :param values: current values of the model to verify @@ -103,7 +110,8 @@ def convert_choices_if_needed( :rtype: Tuple[Any, List] """ value = values.get(field.name, ormar.Undefined) - choices = list(field.choices) + choices = [o.value if isinstance(o, Enum) else o for o in field.choices] + if field.__type__ in [datetime.datetime, datetime.date, datetime.time]: value = value.isoformat() if not isinstance(value, str) else value choices = [o.isoformat() for o in field.choices] @@ -111,7 +119,7 @@ def convert_choices_if_needed( value = str(value) if not isinstance(value, str) else value choices = [str(o) for o in field.choices] elif field.__type__ == decimal.Decimal: - precision = field.precision # type: ignore + precision = field.scale # type: ignore value = ( round(float(value), precision) if isinstance(value, decimal.Decimal) diff --git a/tests/test_choices_schema.py b/tests/test_choices_schema.py index 63456bd..0cf1852 100644 --- a/tests/test_choices_schema.py +++ b/tests/test_choices_schema.py @@ -1,6 +1,7 @@ import datetime import decimal import uuid +from enum import Enum import databases import pydantic @@ -21,6 +22,11 @@ uuid1 = uuid.uuid4() uuid2 = uuid.uuid4() +class TestEnum(Enum): + val1 = "Val1" + val2 = "Val2" + + class Organisation(ormar.Model): class Meta: tablename = "org" @@ -39,18 +45,18 @@ class Organisation(ormar.Model): ) expire_datetime: datetime.datetime = ormar.DateTime( - choices=[datetime.datetime(2021, 1, 1, 10, 0, 0), - datetime.datetime(2022, 5, 1, 12, 30)] + choices=[ + datetime.datetime(2021, 1, 1, 10, 0, 0), + datetime.datetime(2022, 5, 1, 12, 30), + ] ) random_val: float = ormar.Float(choices=[2.0, 3.5]) - random_decimal: decimal.Decimal = ormar.Decimal(scale=4, precision=2, - choices=[decimal.Decimal(12.4), decimal.Decimal(58.2)] + random_decimal: decimal.Decimal = ormar.Decimal( + scale=2, precision=4, choices=[decimal.Decimal(12.4), decimal.Decimal(58.2)] ) - random_json: pydantic.Json = ormar.JSON( - choices=["aa", "{\"aa\":\"bb\"}"] - ) - random_uuid: uuid.UUID = ormar.UUID( - choices=[uuid1, uuid2]) + 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(TestEnum)) @app.on_event("startup") @@ -86,10 +92,7 @@ def test_all_endpoints(): with client as client: response = client.post( "/items/", - json={"id": 1, - "ident": "", - "priority": 4, - "expire_date": "2022-05-01"}, + json={"id": 1, "ident": "", "priority": 4, "expire_date": "2022-05-01"}, ) assert response.status_code == 422 @@ -105,8 +108,9 @@ def test_all_endpoints(): "expire_datetime": "2022-05-01T12:30:00", "random_val": 3.5, "random_decimal": 12.4, - "random_json": "{\"aa\":\"bb\"}", - "random_uuid": str(uuid1) + "random_json": '{"aa":"bb"}', + "random_uuid": str(uuid1), + "enum_string": TestEnum.val1.value, }, ) @@ -132,11 +136,7 @@ def test_schema_gen(): schema = app.openapi() assert "Organisation" in schema["components"]["schemas"] props = schema["components"]["schemas"]["Organisation"]["properties"] - for field in ["ident", "priority", "expire_date"]: + for field in [k for k in Organisation.Meta.model_fields.keys() if k != "id"]: assert "enum" in props.get(field) - choices = Organisation.Meta.model_fields.get(field).choices - assert props.get(field).get("enum") == [ - str(x) if isinstance(x, datetime.date) else x for x in choices - ] assert "description" in props.get(field) assert "An enumeration." in props.get(field).get("description")