-
Notifications
You must be signed in to change notification settings - Fork 5
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
Adding time map kml support #136
base: master
Are you sure you want to change the base?
Changes from 8 commits
5d1a67c
f220331
4fa91d0
135dc73
187cf5e
6ecf92c
d440595
b65bb2a
25a08bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from typing import List | ||
|
||
from traveltimepy.dto.requests.request import TravelTimeRequest | ||
from traveltimepy.dto.requests.time_map import ( | ||
DepartureSearch, | ||
ArrivalSearch, | ||
) | ||
from traveltimepy.dto.responses.time_map_kml import TimeMapKmlResponse | ||
from traveltimepy.itertools import split, flatten | ||
|
||
|
||
class TimeMapRequestKML(TravelTimeRequest[TimeMapKmlResponse]): | ||
departure_searches: List[DepartureSearch] | ||
arrival_searches: List[ArrivalSearch] | ||
|
||
def split_searches(self, window_size: int) -> List[TravelTimeRequest]: | ||
return [ | ||
TimeMapRequestKML( | ||
departure_searches=departures, | ||
arrival_searches=arrivals, | ||
) | ||
for departures, arrivals in split( | ||
self.departure_searches, self.arrival_searches, window_size | ||
) | ||
] | ||
|
||
def merge(self, responses: List[TimeMapKmlResponse]) -> TimeMapKmlResponse: | ||
merged_features = flatten([response.results for response in responses]) | ||
return TimeMapKmlResponse(results=merged_features) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
from typing import List | ||
from fastkml import Placemark, kml | ||
from pydantic import BaseModel | ||
|
||
# The stable version of fastkml is from 2021 and does not have types defined, nor | ||
# a `py.typed` file. There are newer pre-release versions as new as 2024, but they | ||
# seem to be undoccumented. | ||
# TODO: Maybe port this into newer versions of fastkml for proper type checking support | ||
|
||
|
||
class TimeMapKmlResult(BaseModel): | ||
placemark: Placemark | ||
|
||
def search_id(self) -> str: | ||
return self.placemark.name # type: ignore | ||
|
||
def pretty_string(self) -> str: # type: ignore | ||
return self.placemark.to_string(prettyprint=True) | ||
|
||
class Config: | ||
arbitrary_types_allowed = True | ||
|
||
|
||
class TimeMapKmlResponse(BaseModel): | ||
results: List[TimeMapKmlResult] | ||
|
||
|
||
def parse_kml_as(kml_string: str) -> TimeMapKmlResponse: | ||
k = kml.KML() | ||
k.from_string(kml_string.encode("utf-8")) | ||
|
||
results = [ | ||
TimeMapKmlResult(placemark=feature) | ||
for feature in k.features() | ||
if isinstance(feature, kml.Placemark) | ||
] | ||
|
||
return TimeMapKmlResponse(results=results) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,16 @@ | ||
import asyncio | ||
import json | ||
from dataclasses import dataclass | ||
from typing import TypeVar, Type, Dict, Optional | ||
from typing import TypeVar, Type, Dict, Optional, Union | ||
|
||
from aiohttp import ClientSession, ClientResponse, TCPConnector, ClientTimeout | ||
from pydantic import BaseModel | ||
from traveltimepy.dto.requests.request import TravelTimeRequest | ||
|
||
from traveltimepy.dto.responses.time_map_kml import ( | ||
TimeMapKmlResponse, | ||
parse_kml_as, | ||
) | ||
from traveltimepy.dto.responses.error import ResponseError | ||
from traveltimepy.errors import ApiError | ||
from aiohttp_retry import RetryClient, ExponentialRetry | ||
|
@@ -34,12 +38,15 @@ async def send_post_request_async( | |
headers: Dict[str, str], | ||
request: TravelTimeRequest, | ||
rate_limit: AsyncLimiter, | ||
) -> T: | ||
) -> Union[T, TimeMapKmlResponse]: | ||
Comment on lines
-37
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very annoying change to me, cause There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to avoid such hacks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea me too, but I think to avoid it I'd need to make a bigger rework, so wanted to do it in a separate PR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the problem with TimeMapKmlResponse cause it looks the same as other response classes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's a very weird issue, the type checker can't resolve that TimeMapKmlResponse can fit into T. |
||
async with rate_limit: | ||
async with client.post( | ||
url=url, headers=headers, data=request.model_dump_json() | ||
) as resp: | ||
return await _process_response(response_class, resp) | ||
if response_class == TimeMapKmlResponse: | ||
return await _process_kml_response(resp) | ||
else: | ||
return await _process_json_response(response_class, resp) | ||
|
||
|
||
async def send_post_async( | ||
|
@@ -103,20 +110,35 @@ async def send_get_async( | |
headers=headers, | ||
params=params, | ||
) as resp: | ||
return await _process_response(response_class, resp) | ||
return await _process_json_response(response_class, resp) | ||
|
||
|
||
def _handle_non_ok_response(json_data): | ||
parsed = ResponseError.model_validate_json(json.dumps(json_data)) | ||
msg = ( | ||
f"Travel Time API request failed: {parsed.description}\n" | ||
f"Error code: {parsed.error_code}\n" | ||
f"Additional info: {parsed.additional_info}\n" | ||
f"<{parsed.documentation_link}>\n" | ||
) | ||
raise ApiError(msg) | ||
|
||
|
||
async def _process_response(response_class: Type[T], response: ClientResponse) -> T: | ||
async def _process_json_response( | ||
response_class: Type[T], response: ClientResponse | ||
) -> T: | ||
text = await response.text() | ||
json_data = json.loads(text) | ||
if response.status != 200: | ||
parsed = ResponseError.model_validate_json(json.dumps(json_data)) | ||
msg = ( | ||
f"Travel Time API request failed: {parsed.description}\n" | ||
f"Error code: {parsed.error_code}\n" | ||
f"Additional info: {parsed.additional_info}\n" | ||
f"<{parsed.documentation_link}>\n" | ||
) | ||
raise ApiError(msg) | ||
return _handle_non_ok_response(json_data) | ||
else: | ||
return response_class.model_validate(json_data) | ||
|
||
|
||
async def _process_kml_response(response: ClientResponse) -> TimeMapKmlResponse: | ||
text = await response.text() | ||
if response.status != 200: | ||
json_data = json.loads(text) | ||
return _handle_non_ok_response(json_data) | ||
else: | ||
return parse_kml_as(text) |
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.
Regarding this, the current version is
0.12
. They have 1.0 alpha versions available, seems like they have been slowly developing 1.0 for ~3 years now. It seems pretty vastly different and not documented, so for now I'll keep using the stable version with type checking disabled.