-
Notifications
You must be signed in to change notification settings - Fork 6
[FLAPI-2098] Add type annotations for models #79
Conversation
e9c8acd
to
b8e630b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably a good moment to start adding unit tests for the model classes as well.
b8e630b
to
8b441a1
Compare
da0c8c8
to
24b596b
Compare
@sgerrand: Specifically for the |
Yes, please. |
47a9297
to
7e53c46
Compare
refund_currency=json["refund_currency"], | ||
refund_to=json["refund_to"], | ||
confirmed_at=parse_datetime(json["confirmed_at"]), | ||
created_at=datetime.strptime(json["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this meant to use the new parse_datetime
function (below)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not. That's for a value that requires for checking the length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like we may as well use it for consistency even if we don't actually need to check the length, since this case is handled in it - what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just realised (d'oh) that the parse_datetime
function is defined in this module! I think it'd be worth moving it to utils and using it every time we parse a datetime?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Unless we want to explicitly error in the case where the precision isn't as expected for a given field?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not all date time values are the same format. There would need to be a key
passed along to some shared function (like done previously), but I'm not a fan of that—or there could be n
different parse date time value functions, which I'm also not a fan of.
I'll keep it as it is to avoid too early DRY'ing it out.
duffel_api/models/place.py
Outdated
time_zone=json.get("time_zone"), | ||
city_name=json.get("city_name"), | ||
city=get_and_transform(json, "city", City.from_json), | ||
airports=get_and_transform( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above,
airports=get_and_transform(
json,
"airports",
lambda value: [Airport.from_json(airport) for airport in value],
),
could become
airports=[Airport.from_json(a) for a in value] if "airports" in json else None
depending on which you prefer!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd need to do an equivalent for the line above (city=get_and_transform(json, "city", City.from_json),
), which doesn't lend itself as nicely since it's not a list comprehension?
38281bc
to
efb3844
Compare
a91b2c5
to
7ac54f7
Compare
2c74a93
to
59f28ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One early question, still reviewing!
It looks like you're still actively pushing changes so I'll hold off for now and review this tomorrow. |
@sgerrand: Sorry, yup! I've paused for today though, so it's safe. I'll do fixup commits tomorrow too to preserve the review comments. |
@@ -61,7 +61,7 @@ def __iter__(self): | |||
while "meta" in response: | |||
after = response["meta"]["after"] | |||
for entry in response["data"]: | |||
yield self._caller(entry) | |||
yield self._caller.from_json(entry) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@richcooper95: I'm curious how we could ensure that the thing here being called has the from_json
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If self._caller
is the class we want to instantiate, and we only want to allow self._caller
to be set if it contains a from_json
classmethod, then we could add validation* to the self._caller
attribute - something along the lines of:
from inspect import getattr_static
try:
assert isinstance(getattr_static(x, "from_json"), classmethod)
except (AttributeError, AssertionError) as _:
raise TypeError("'caller' must have a 'from_json' classmethod") from None
*might be a good opportunity to try this method of validation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jesse-c: Did you have any thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to reply by email, but it hasn't shown up!
Basically, I'm going to leave this for another PR, and have a proper think about it.
d1a8133
to
ffcf61e
Compare
We need to refactor our classes to avoid circular dependencies.
ffcf61e
to
7fe1271
Compare
I was going to worry about it in another PR since this once has become quite large.
… On 28 Jan 2022, at 18:11, Rich Cooper ***@***.***> wrote:
@richcooper95 commented on this pull request.
In duffel_api/http_client.py:
> @@ -61,7 +61,7 @@ def __iter__(self):
while "meta" in response:
after = response["meta"]["after"]
for entry in response["data"]:
- yield self._caller(entry)
+ yield self._caller.from_json(entry)
@jesse-c: Did you have any thoughts on this?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments that might be worth thinking about while the hood's up, but it looks so good - huge improvement to user experience! ❤️
@@ -15,7 +15,9 @@ def __init__(self, **kwargs): | |||
|
|||
def get(self, id_): | |||
"""GET /air/offer_requests/:id""" | |||
return OfferRequest(self.do_get("{}/{}".format(self._url, id_))["data"]) | |||
return OfferRequest.from_json( | |||
self.do_get("{}/{}".format(self._url, id_))["data"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're probably going to move to f-strings over the codebase, may as well start here:
self.do_get(f"{self._url}/{id_}"))["data"]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave the f-strings for another PR!
@@ -20,7 +20,9 @@ def __init__(self, **kwargs): | |||
|
|||
def get(self, id_): | |||
"""GET /air/order_cancellations/:id.""" | |||
return OrderCancellation(self.do_get("{}/{}".format(self._url, id_))["data"]) | |||
return OrderCancellation.from_json( | |||
self.do_get("{}/{}".format(self._url, id_))["data"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto about f-strings
@@ -11,7 +11,9 @@ def __init__(self, **kwargs): | |||
|
|||
def get(self, id_): | |||
"""GET /air/order_change_offers/:id""" | |||
return OrderChangeOffer(self.do_get("{}/{}".format(self._url, id_))["data"]) | |||
return OrderChangeOffer.from_json( | |||
self.do_get("{}/{}".format(self._url, id_))["data"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f-strings
@@ -16,7 +16,9 @@ def create(self, order_id): | |||
|
|||
def get(self, id_): | |||
"""GET /air/order_change_requests/:id""" | |||
return OrderChangeRequest(self.do_get("{}/{}".format(self._url, id_))["data"]) | |||
return OrderChangeRequest.from_json( | |||
self.do_get("{}/{}".format(self._url, id_))["data"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f-strings
@@ -26,7 +26,9 @@ def __init__(self, **kwargs): | |||
|
|||
def get(self, id_): | |||
"""GET /air/order_changes/:id.""" | |||
return OrderChange(self.do_get("{}/{}".format(self._url, id_))["data"]) | |||
return OrderChange.from_json( | |||
self.do_get("{}/{}".format(self._url, id_))["data"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f-strings
from .airline import Airline | ||
from .airport import Airport, City, Place, Refund |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely want to change this - sorry I haven't got round to a better solution yet! I'll add an issue for it and assign it to myself so I don't forget!
refund_currency=json["refund_currency"], | ||
refund_to=json["refund_to"], | ||
confirmed_at=parse_datetime(json["confirmed_at"]), | ||
created_at=datetime.strptime(json["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like we may as well use it for consistency even if we don't actually need to check the length, since this case is handled in it - what do you think?
refund_currency=json["refund_currency"], | ||
refund_to=json["refund_to"], | ||
confirmed_at=parse_datetime(json["confirmed_at"]), | ||
created_at=datetime.strptime(json["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just realised (d'oh) that the parse_datetime
function is defined in this module! I think it'd be worth moving it to utils and using it every time we parse a datetime?
refund_currency=json["refund_currency"], | ||
refund_to=json["refund_to"], | ||
confirmed_at=parse_datetime(json["confirmed_at"]), | ||
created_at=datetime.strptime(json["created_at"], "%Y-%m-%dT%H:%M:%S.%fZ"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Unless we want to explicitly error in the case where the precision isn't as expected for a given field?)
|
||
OrderChangeOfferSlicesRemove = OrderChangeOfferSlicesAdd | ||
def parse_datetime(value: str) -> datetime: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above about defining this in utils and using everywhere!
This is a follow-on from previous changes (e.g. #79). I used Comby [1] for most of the changes. [1] `$ comby -i '"{}/{}:[rest]".format(:[arg1], :[arg2])' 'f"{:[arg1]}/{:[arg2]}:[rest]"' .py`
The type annotations are added for all models. We use the
dataclass
to give us a consistent approach. We've added afrom_json
function specifically for that case.I've chosen for now to not standardise the
datetime
again. If we discover a sane pattern to this, I'm happy to refactor it later.Closes #81
Closes #82