Skip to content

feat: add UUID format for string properties #1131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions end_to_end_tests/baseline_openapi_3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,8 @@
"aCamelDateTime",
"a_date",
"a_nullable_date",
"a_uuid",
"a_nullable_uuid",
"required_nullable",
"required_not_nullable",
"model",
Expand Down Expand Up @@ -1784,6 +1786,22 @@
"type": "string",
"format": "date"
},
"a_uuid": {
"title": "A Uuid",
"type": "string",
"format": "uuid"
},
"a_nullable_uuid": {
"title": "A Nullable Uuid",
"type": "string",
"format": "uuid",
"nullable": true
},
"a_not_required_uuid": {
"title": "A Not Required Uuid",
"type": "string",
"format": "uuid"
},
"1_leading_digit": {
"title": "Leading Digit",
"type": "string"
Expand Down
24 changes: 24 additions & 0 deletions end_to_end_tests/baseline_openapi_3.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,8 @@ info:
"aCamelDateTime",
"a_date",
"a_nullable_date",
"a_uuid",
"a_nullable_uuid",
"required_nullable",
"required_not_nullable",
"model",
Expand Down Expand Up @@ -1771,6 +1773,28 @@ info:
"type": "string",
"format": "date"
},
"a_uuid": {
"title": "A Uuid",
"type": "string",
"format": "uuid"
},
"a_nullable_uuid": {
"title": "A Nullable Uuid",
"anyOf": [
{
"type": "string",
"format": "uuid"
},
{
"type": "null"
}
]
},
"a_not_required_uuid": {
"title": "A Not Required Uuid",
"type": "string",
"format": "uuid"
},
"1_leading_digit": {
"title": "Leading Digit",
"type": "string"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import uuid
from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast

from attrs import define as _attrs_define
Expand Down Expand Up @@ -27,6 +28,8 @@ class AModel:
a_camel_date_time (Union[datetime.date, datetime.datetime]):
a_date (datetime.date):
a_nullable_date (Union[None, datetime.date]):
a_uuid (uuid.UUID):
a_nullable_uuid (Union[None, uuid.UUID]):
required_nullable (Union[None, str]):
required_not_nullable (str):
one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]):
Expand All @@ -37,6 +40,7 @@ class AModel:
an_optional_allof_enum (Union[Unset, AnAllOfEnum]):
nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]):
a_not_required_date (Union[Unset, datetime.date]):
a_not_required_uuid (Union[Unset, uuid.UUID]):
attr_1_leading_digit (Union[Unset, str]):
attr_leading_underscore (Union[Unset, str]):
not_required_nullable (Union[None, Unset, str]):
Expand All @@ -51,6 +55,8 @@ class AModel:
a_camel_date_time: Union[datetime.date, datetime.datetime]
a_date: datetime.date
a_nullable_date: Union[None, datetime.date]
a_uuid: uuid.UUID
a_nullable_uuid: Union[None, uuid.UUID]
required_nullable: Union[None, str]
required_not_nullable: str
one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any]
Expand All @@ -62,6 +68,7 @@ class AModel:
an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
a_not_required_date: Union[Unset, datetime.date] = UNSET
a_not_required_uuid: Union[Unset, uuid.UUID] = UNSET
attr_1_leading_digit: Union[Unset, str] = UNSET
attr_leading_underscore: Union[Unset, str] = UNSET
not_required_nullable: Union[None, Unset, str] = UNSET
Expand Down Expand Up @@ -93,6 +100,14 @@ def to_dict(self) -> Dict[str, Any]:
else:
a_nullable_date = self.a_nullable_date

a_uuid = str(self.a_uuid)

a_nullable_uuid: Union[None, str]
if isinstance(self.a_nullable_uuid, uuid.UUID):
a_nullable_uuid = str(self.a_nullable_uuid)
else:
a_nullable_uuid = self.a_nullable_uuid

required_nullable: Union[None, str]
required_nullable = self.required_nullable

Expand Down Expand Up @@ -143,6 +158,10 @@ def to_dict(self) -> Dict[str, Any]:
if not isinstance(self.a_not_required_date, Unset):
a_not_required_date = self.a_not_required_date.isoformat()

a_not_required_uuid: Union[Unset, str] = UNSET
if not isinstance(self.a_not_required_uuid, Unset):
a_not_required_uuid = str(self.a_not_required_uuid)

attr_1_leading_digit = self.attr_1_leading_digit

attr_leading_underscore = self.attr_leading_underscore
Expand Down Expand Up @@ -193,6 +212,8 @@ def to_dict(self) -> Dict[str, Any]:
"aCamelDateTime": a_camel_date_time,
"a_date": a_date,
"a_nullable_date": a_nullable_date,
"a_uuid": a_uuid,
"a_nullable_uuid": a_nullable_uuid,
"required_nullable": required_nullable,
"required_not_nullable": required_not_nullable,
"one_of_models": one_of_models,
Expand All @@ -209,6 +230,8 @@ def to_dict(self) -> Dict[str, Any]:
field_dict["nested_list_of_enums"] = nested_list_of_enums
if a_not_required_date is not UNSET:
field_dict["a_not_required_date"] = a_not_required_date
if a_not_required_uuid is not UNSET:
field_dict["a_not_required_uuid"] = a_not_required_uuid
if attr_1_leading_digit is not UNSET:
field_dict["1_leading_digit"] = attr_1_leading_digit
if attr_leading_underscore is not UNSET:
Expand Down Expand Up @@ -272,6 +295,23 @@ def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]:

a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date"))

a_uuid = uuid.UUID(d.pop("a_uuid"))

def _parse_a_nullable_uuid(data: object) -> Union[None, uuid.UUID]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
a_nullable_uuid_type_0 = uuid.UUID(data)

return a_nullable_uuid_type_0
except: # noqa: E722
pass
return cast(Union[None, uuid.UUID], data)

a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid"))

def _parse_required_nullable(data: object) -> Union[None, str]:
if data is None:
return data
Expand Down Expand Up @@ -370,6 +410,13 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]
else:
a_not_required_date = isoparse(_a_not_required_date).date()

_a_not_required_uuid = d.pop("a_not_required_uuid", UNSET)
a_not_required_uuid: Union[Unset, uuid.UUID]
if isinstance(_a_not_required_uuid, Unset):
a_not_required_uuid = UNSET
else:
a_not_required_uuid = uuid.UUID(_a_not_required_uuid)

attr_1_leading_digit = d.pop("1_leading_digit", UNSET)

attr_leading_underscore = d.pop("_leading_underscore", UNSET)
Expand Down Expand Up @@ -463,6 +510,8 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro
a_camel_date_time=a_camel_date_time,
a_date=a_date,
a_nullable_date=a_nullable_date,
a_uuid=a_uuid,
a_nullable_uuid=a_nullable_uuid,
required_nullable=required_nullable,
required_not_nullable=required_not_nullable,
one_of_models=one_of_models,
Expand All @@ -473,6 +522,7 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro
an_optional_allof_enum=an_optional_allof_enum,
nested_list_of_enums=nested_list_of_enums,
a_not_required_date=a_not_required_date,
a_not_required_uuid=a_not_required_uuid,
attr_1_leading_digit=attr_1_leading_digit,
attr_leading_underscore=attr_leading_underscore,
not_required_nullable=not_required_nullable,
Expand Down
12 changes: 11 additions & 1 deletion openapi_python_client/parser/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@
)
from .string import StringProperty
from .union import UnionProperty
from .uuid import UuidProperty


def _string_based_property(
name: str, required: bool, data: oai.Schema, config: Config
) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | PropertyError:
) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | UuidProperty | PropertyError:
"""Construct a Property from the type "string" """
string_format = data.schema_format
python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix)
Expand Down Expand Up @@ -79,6 +80,15 @@ def _string_based_property(
description=data.description,
example=data.example,
)
if string_format == "uuid":
return UuidProperty.build(
name=name,
required=required,
default=None,
python_name=python_name,
description=data.description,
example=data.example,
)
return StringProperty.build(
name=name,
default=data.default,
Expand Down
2 changes: 2 additions & 0 deletions openapi_python_client/parser/properties/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .none import NoneProperty
from .string import StringProperty
from .union import UnionProperty
from .uuid import UuidProperty

Property: TypeAlias = Union[
AnyProperty,
Expand All @@ -34,4 +35,5 @@
NoneProperty,
StringProperty,
UnionProperty,
UuidProperty,
]
82 changes: 82 additions & 0 deletions openapi_python_client/parser/properties/uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

from typing import Any, ClassVar
from uuid import UUID

from attr import define

from ... import schema as oai
from ...utils import PythonIdentifier
from ..errors import PropertyError
from .protocol import PropertyProtocol, Value


@define
class UuidProperty(PropertyProtocol):
"""A property of type uuid.UUID"""

name: str
required: bool
default: Value | None
python_name: PythonIdentifier
description: str | None
example: str | None

_type_string: ClassVar[str] = "uuid.UUID"
_json_type_string: ClassVar[str] = "str"
_allowed_locations: ClassVar[set[oai.ParameterLocation]] = {
oai.ParameterLocation.QUERY,
oai.ParameterLocation.PATH,
oai.ParameterLocation.COOKIE,
oai.ParameterLocation.HEADER,
}
template: ClassVar[str] = "uuid_property.py.jinja"

@classmethod
def build(
cls,
name: str,
required: bool,
default: Any,
python_name: PythonIdentifier,
description: str | None,
example: str | None,
) -> UuidProperty | PropertyError:
checked_default = cls.convert_value(default)
if isinstance(checked_default, PropertyError):
return checked_default

return cls(
name=name,
required=required,
default=checked_default,
python_name=python_name,
description=description,
example=example,
)

@classmethod
def convert_value(cls, value: Any) -> Value | None | PropertyError:
if value is None or isinstance(value, Value):
return value
if isinstance(value, str):
try:
UUID(value)
except ValueError:
return PropertyError(f"Invalid UUID value: {value}")
return Value(value)
if isinstance(value, UUID):
return Value(str(value))
return PropertyError(f"Invalid UUID value: {value}")

def get_imports(self, *, prefix: str) -> set[str]:
"""
Get a set of import strings that should be included when this property is used somewhere

Args:
prefix: A prefix to put before any relative (local) module names. This should be the number of . to get
back to the root of the generated client.
"""
imports = super().get_imports(prefix=prefix)
imports.update({"import uuid"})
return imports
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% macro construct_function(property, source) %}
uuid.UUID({{ source }})
{% endmacro %}

{% from "property_templates/property_macros.py.jinja" import construct_template %}

{% macro construct(property, source) %}
{{ construct_template(construct_function, property, source) }}
{% endmacro %}

{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %}

{% macro transform(property, source, destination, declare_type=True) %}
{% set transformed = "str(" + source + ")" %}
{% if property.required %}
{{ destination }} = {{ transformed }}
{%- else %}
{% if declare_type %}
{% set type_annotation = property.get_type_string(json=True) %}
{{ destination }}: {{ type_annotation }} = UNSET
{% else %}
{{ destination }} = UNSET
{% endif %}
if not isinstance({{ source }}, Unset):
{{ destination }} = {{ transformed }}
{%- endif %}
{% endmacro %}

{% macro transform_multipart(property, source, destination) %}
{% if property.required %}
{{ destination }} = str({{ source }})
{%- else %}
{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %}
{{ destination }}: {{ type_annotation }} = UNSET
if not isinstance({{ source }}, Unset):
{{ destination }} = str({{ source }})
{%- endif %}
{% endmacro %}
Loading