Skip to content

Add support for date properties #37

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

Merged
merged 7 commits into from
Apr 25, 2020
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ dmypy.json

/coverage.xml
/.coverage
htmlcov/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 0.3.0 - Unreleased
### Additions
- Link to the GitHub repository from PyPI (#26). Thanks @theY4Kman!
- Support for date properties (#30, #37). Thanks @acgray!

### Fixes
- Fixed some typing issues in generated clients and incorporate mypy into end to end tests (#32). Thanks @acgray!
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ OpenAPI document.
## OpenAPI features supported
1. All HTTP Methods
1. JSON and form bodies, path and query parameters
1. float, string, int, datetimes, string enums, and custom schemas or lists containing any of those
1. float, string, int, date, datetime, string enums, and custom schemas or lists containing any of those
1. html/text or application/json responses containing any of the previous types
1. Bearer token security

Expand Down
24 changes: 21 additions & 3 deletions openapi_python_client/openapi_parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@
from enum import Enum
from typing import Any, Dict, Generator, Iterable, List, Optional, Set, Union

from .properties import EnumListProperty, EnumProperty, Property, ReferenceListProperty, RefProperty, property_from_dict
from .properties import (
DateProperty,
DateTimeProperty,
EnumListProperty,
EnumProperty,
Property,
ReferenceListProperty,
RefProperty,
property_from_dict,
)
from .reference import Reference
from .responses import ListRefResponse, RefResponse, Response, response_from_dict

Expand Down Expand Up @@ -108,11 +117,16 @@ def _add_parameters(self, data: Dict[str, Any]) -> None:
prop = property_from_dict(
name=param_dict["name"], required=param_dict["required"], data=param_dict["schema"]
)
if (
if isinstance(prop, DateProperty):
self.relative_imports.add("from datetime import date")
elif isinstance(prop, DateTimeProperty):
self.relative_imports.add("from datetime import datetime")
elif (
isinstance(prop, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty))
and prop.reference
):
self.relative_imports.add(import_string_from_reference(prop.reference, prefix="..models"))

if param_dict["in"] == ParameterLocation.QUERY:
self.query_parameters.append(prop)
elif param_dict["in"] == ParameterLocation.PATH:
Expand Down Expand Up @@ -168,7 +182,11 @@ def from_dict(d: Dict[str, Any], /) -> Schema:
required_properties.append(p)
else:
optional_properties.append(p)
if isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference:
if isinstance(p, DateTimeProperty):
relative_imports.add("from datetime import datetime")
elif isinstance(p, DateProperty):
relative_imports.add("from datetime import date")
elif isinstance(p, (ReferenceListProperty, EnumListProperty, RefProperty, EnumProperty)) and p.reference:
relative_imports.add(import_string_from_reference(p.reference))
schema = Schema(
reference=Reference.from_ref(d["title"]),
Expand Down
28 changes: 22 additions & 6 deletions openapi_python_client/openapi_parser/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ class DateTimeProperty(Property):
_type_string: ClassVar[str] = "datetime"
constructor_template: ClassVar[str] = "datetime_property.pyi"

def transform(self) -> str:
return f"{self.python_name}.isoformat()"


@dataclass
class DateProperty(Property):
""" A property of type datetime.date """

_type_string: ClassVar[str] = "date"
constructor_template: ClassVar[str] = "date_property.pyi"

def transform(self) -> str:
return f"{self.python_name}.isoformat()"


@dataclass
class FloatProperty(Property):
Expand Down Expand Up @@ -245,12 +259,14 @@ def property_from_dict(name: str, required: bool, data: Dict[str, Any]) -> Prope
if "$ref" in data:
return RefProperty(name=name, required=required, reference=Reference.from_ref(data["$ref"]), default=None)
if data["type"] == "string":
if "format" not in data:
return StringProperty(
name=name, default=data.get("default"), required=required, pattern=data.get("pattern"),
)
elif data["format"] == "date-time":
return DateTimeProperty(name=name, required=required, default=data.get("default"))
if "format" in data:
if data.get("format") == "date-time":
return DateTimeProperty(name=name, required=required, default=data.get("default"))
elif data.get("format") == "date":
return DateProperty(name=name, required=required, default=data.get("default"))
else:
raise ValueError(f'Unsupported string format:{data["format"]}')
return StringProperty(name=name, default=data.get("default"), required=required, pattern=data.get("pattern"),)
elif data["type"] == "number":
return FloatProperty(name=name, default=data.get("default"), required=required)
elif data["type"] == "integer":
Expand Down
7 changes: 7 additions & 0 deletions openapi_python_client/templates/date_property.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% if property.required %}
{{ property.name }} = date.fromisoformat(d["{{ property.name }}"])
{% else %}
{{ property.name }} = None
if ({{ property.name }}_string := d.get("{{ property.name }}")) is not None:
{{ property.name }} = date.fromisoformat(cast(str, {{ property.name }}_string))
{% endif %}
1 change: 0 additions & 1 deletion openapi_python_client/templates/model.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, cast

{% for relative in schema.relative_imports %}
Expand Down
5 changes: 3 additions & 2 deletions tests/test_end_to_end/fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
""" A FastAPI app used to create an OpenAPI document for end-to-end testing """
import json
from datetime import datetime
from datetime import date, datetime
from enum import Enum
from pathlib import Path
from typing import List
Expand Down Expand Up @@ -45,10 +45,11 @@ class AModel(BaseModel):
a_list_of_strings: List[str]
a_list_of_objects: List[OtherModel]
aCamelDateTime: datetime
a_date: date


@test_router.get("/", response_model=List[AModel], operation_id="getUserList")
def get_list(statuses: List[AnEnum] = Query(...),):
def get_list(statuses: List[AnEnum] = Query(...), some_date: date = Query(...), some_datetime: datetime = Query(...)):
""" Get users, filtered by statuses """
return

Expand Down
28 changes: 27 additions & 1 deletion tests/test_end_to_end/fastapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@
},
"name": "statuses",
"in": "query"
},
{
"required": true,
"schema": {
"title": "Some Date",
"type": "string",
"format": "date"
},
"name": "some_date",
"in": "query"
},
{
"required": true,
"schema": {
"title": "Some Datetime",
"type": "string",
"format": "date-time"
},
"name": "some_datetime",
"in": "query"
}
],
"responses": {
Expand Down Expand Up @@ -88,7 +108,8 @@
"a_list_of_enums",
"a_list_of_strings",
"a_list_of_objects",
"aCamelDateTime"
"aCamelDateTime",
"a_date"
],
"type": "object",
"properties": {
Expand Down Expand Up @@ -127,6 +148,11 @@
"title": "Acameldatetime",
"type": "string",
"format": "date-time"
},
"a_date": {
"title": "A Date",
"type": "string",
"format": "date"
}
},
"description": "A Model for testing all the ways custom objects can be used "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import asdict
from datetime import date, datetime
from typing import Any, Dict, List, Optional, Union, cast

import httpx
Expand All @@ -11,7 +12,7 @@


def get_user_list(
*, client: Client, statuses: List[Statuses],
*, client: Client, statuses: List[Statuses], some_date: date, some_datetime: datetime,
) -> Union[
List[AModel], HTTPValidationError,
]:
Expand All @@ -20,6 +21,8 @@ def get_user_list(

params = {
"statuses": statuses,
"some_date": some_date.isoformat(),
"some_datetime": some_datetime.isoformat(),
}

response = httpx.get(url=url, headers=client.get_headers(), params=params,)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import asdict
from datetime import date, datetime
from typing import Any, Dict, List, Optional, Union, cast

import httpx
Expand All @@ -11,7 +12,7 @@


async def get_user_list(
*, client: Client, statuses: List[Statuses],
*, client: Client, statuses: List[Statuses], some_date: date, some_datetime: datetime,
) -> Union[
List[AModel], HTTPValidationError,
]:
Expand All @@ -20,6 +21,8 @@ async def get_user_list(

params = {
"statuses": statuses,
"some_date": some_date.isoformat(),
"some_datetime": some_datetime.isoformat(),
}

async with httpx.AsyncClient() as _client:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from datetime import date, datetime
from typing import Any, Dict, List, Optional, cast

from .a_list_of_enums import AListOfEnums
Expand All @@ -18,14 +18,16 @@ class AModel:
a_list_of_strings: List[str]
a_list_of_objects: List[OtherModel]
a_camel_date_time: datetime
a_date: date

def to_dict(self) -> Dict[str, Any]:
return {
"an_enum_value": self.an_enum_value.value,
"a_list_of_enums": self.a_list_of_enums,
"a_list_of_strings": self.a_list_of_strings,
"a_list_of_objects": self.a_list_of_objects,
"aCamelDateTime": self.a_camel_date_time,
"aCamelDateTime": self.a_camel_date_time.isoformat(),
"a_date": self.a_date.isoformat(),
}

@staticmethod
Expand All @@ -45,10 +47,13 @@ def from_dict(d: Dict[str, Any]) -> AModel:

a_camel_date_time = datetime.fromisoformat(d["aCamelDateTime"])

a_date = date.fromisoformat(d["a_date"])

return AModel(
an_enum_value=an_enum_value,
a_list_of_enums=a_list_of_enums,
a_list_of_strings=a_list_of_strings,
a_list_of_objects=a_list_of_objects,
a_camel_date_time=a_camel_date_time,
a_date=a_date,
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, cast


Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, cast

from .validation_error import ValidationError
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, cast


Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, cast


Expand Down
Loading