bookalimo 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bookalimo/client.py +45 -22
- bookalimo/integrations/google_places/client_async.py +56 -102
- bookalimo/integrations/google_places/client_sync.py +56 -100
- bookalimo/integrations/google_places/common.py +290 -12
- bookalimo/integrations/google_places/resolve_airport.py +148 -119
- bookalimo/integrations/google_places/transports.py +14 -7
- bookalimo/logging.py +103 -0
- bookalimo/schemas/__init__.py +121 -35
- bookalimo/schemas/base.py +74 -14
- bookalimo/schemas/places/__init__.py +3 -1
- bookalimo/schemas/places/common.py +1 -1
- bookalimo/schemas/places/field_mask.py +0 -9
- bookalimo/schemas/places/google.py +165 -10
- bookalimo/schemas/requests.py +214 -0
- bookalimo/schemas/responses.py +196 -0
- bookalimo/schemas/{booking.py → shared.py} +55 -218
- bookalimo/services/pricing.py +9 -129
- bookalimo/services/reservations.py +10 -100
- bookalimo/transport/auth.py +2 -2
- bookalimo/transport/httpx_async.py +41 -125
- bookalimo/transport/httpx_sync.py +30 -109
- bookalimo/transport/utils.py +204 -3
- bookalimo-1.0.2.dist-info/METADATA +245 -0
- bookalimo-1.0.2.dist-info/RECORD +40 -0
- bookalimo-1.0.1.dist-info/METADATA +0 -370
- bookalimo-1.0.1.dist-info/RECORD +0 -38
- {bookalimo-1.0.1.dist-info → bookalimo-1.0.2.dist-info}/WHEEL +0 -0
- {bookalimo-1.0.1.dist-info → bookalimo-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-1.0.1.dist-info → bookalimo-1.0.2.dist-info}/top_level.txt +0 -0
bookalimo/schemas/__init__.py
CHANGED
@@ -1,49 +1,23 @@
|
|
1
1
|
"""Pydantic schemas for the Bookalimo SDK."""
|
2
2
|
|
3
3
|
from ..transport.auth import Credentials
|
4
|
-
from .booking import (
|
5
|
-
Address,
|
6
|
-
Airport,
|
7
|
-
BookRequest,
|
8
|
-
BookResponse,
|
9
|
-
CardHolderType,
|
10
|
-
City,
|
11
|
-
CreditCard,
|
12
|
-
DetailsRequest,
|
13
|
-
DetailsResponse,
|
14
|
-
EditableReservationRequest,
|
15
|
-
EditReservationResponse,
|
16
|
-
GetReservationRequest,
|
17
|
-
ListReservationsRequest,
|
18
|
-
ListReservationsResponse,
|
19
|
-
Location,
|
20
|
-
LocationType,
|
21
|
-
MeetGreetAdditional,
|
22
|
-
MeetGreetType,
|
23
|
-
Passenger,
|
24
|
-
Price,
|
25
|
-
PriceRequest,
|
26
|
-
PriceResponse,
|
27
|
-
RateType,
|
28
|
-
Reservation,
|
29
|
-
ReservationStatus,
|
30
|
-
Reward,
|
31
|
-
RewardType,
|
32
|
-
Stop,
|
33
|
-
)
|
34
4
|
from .places import (
|
35
5
|
AutocompletePlacesRequest,
|
36
6
|
AutocompletePlacesResponse,
|
7
|
+
BusinessStatus,
|
37
8
|
Circle,
|
38
9
|
EVConnectorType,
|
10
|
+
FieldMaskInput,
|
39
11
|
FormattableText,
|
40
12
|
GeocodingRequest,
|
41
13
|
GetPlaceRequest,
|
14
|
+
GooglePlace,
|
42
15
|
LocationBias,
|
43
16
|
LocationRestriction,
|
44
17
|
Place,
|
45
18
|
PlacePrediction,
|
46
19
|
PlaceType,
|
20
|
+
PriceLevel,
|
47
21
|
QueryPrediction,
|
48
22
|
RankPreference,
|
49
23
|
SearchTextRequest,
|
@@ -52,35 +26,142 @@ from .places import (
|
|
52
26
|
Suggestion,
|
53
27
|
)
|
54
28
|
|
29
|
+
# Import request models (for API calls - serialize to camelCase)
|
30
|
+
from .requests import (
|
31
|
+
Account,
|
32
|
+
Address,
|
33
|
+
Airport,
|
34
|
+
BookRequest,
|
35
|
+
City,
|
36
|
+
CreditCard,
|
37
|
+
DetailsRequest,
|
38
|
+
EditReservationRequest,
|
39
|
+
GetReservationRequest,
|
40
|
+
ListReservationsRequest,
|
41
|
+
Location,
|
42
|
+
MeetGreetAdditional,
|
43
|
+
Passenger,
|
44
|
+
Price,
|
45
|
+
PriceRequest,
|
46
|
+
Reservation,
|
47
|
+
Reward,
|
48
|
+
Stop,
|
49
|
+
)
|
50
|
+
|
51
|
+
# Import response versions with explicit naming for clarity
|
52
|
+
from .responses import (
|
53
|
+
Account as AccountResponse,
|
54
|
+
)
|
55
|
+
from .responses import (
|
56
|
+
Address as AddressResponse,
|
57
|
+
)
|
58
|
+
from .responses import (
|
59
|
+
Airport as AirportResponse,
|
60
|
+
)
|
61
|
+
|
62
|
+
# Import response models (from API responses - serialize to snake_case)
|
63
|
+
from .responses import (
|
64
|
+
BookResponse,
|
65
|
+
CarClassPrice,
|
66
|
+
DetailsResponse,
|
67
|
+
EditReservationResponse,
|
68
|
+
GetReservationResponse,
|
69
|
+
ListReservationsResponse,
|
70
|
+
PriceResponse,
|
71
|
+
ReservationData,
|
72
|
+
)
|
73
|
+
from .responses import (
|
74
|
+
BreakdownItem as BreakdownItemResponse,
|
75
|
+
)
|
76
|
+
from .responses import (
|
77
|
+
City as CityResponse,
|
78
|
+
)
|
79
|
+
from .responses import (
|
80
|
+
CreditCard as CreditCardResponse,
|
81
|
+
)
|
82
|
+
from .responses import (
|
83
|
+
Location as LocationResponse,
|
84
|
+
)
|
85
|
+
from .responses import (
|
86
|
+
MeetGreet as MeetGreetResponse,
|
87
|
+
)
|
88
|
+
from .responses import (
|
89
|
+
MeetGreetAdditional as MeetGreetAdditionalResponse,
|
90
|
+
)
|
91
|
+
from .responses import (
|
92
|
+
Passenger as PassengerResponse,
|
93
|
+
)
|
94
|
+
from .responses import (
|
95
|
+
Reservation as ReservationResponse,
|
96
|
+
)
|
97
|
+
from .responses import (
|
98
|
+
Reward as RewardResponse,
|
99
|
+
)
|
100
|
+
from .responses import (
|
101
|
+
Stop as StopResponse,
|
102
|
+
)
|
103
|
+
|
104
|
+
# Import enums and shared types from shared module
|
105
|
+
from .shared import (
|
106
|
+
CardHolderType,
|
107
|
+
LocationType,
|
108
|
+
MeetGreetType,
|
109
|
+
RateType,
|
110
|
+
ReservationStatus,
|
111
|
+
RewardType,
|
112
|
+
)
|
113
|
+
|
55
114
|
__all__ = [
|
115
|
+
# Enums and shared types
|
56
116
|
"RateType",
|
57
117
|
"LocationType",
|
58
118
|
"MeetGreetType",
|
59
119
|
"RewardType",
|
60
120
|
"ReservationStatus",
|
61
121
|
"CardHolderType",
|
122
|
+
# Request models (default exports - serialize to camelCase for API)
|
62
123
|
"City",
|
63
124
|
"Address",
|
64
125
|
"Airport",
|
65
126
|
"Location",
|
66
127
|
"Stop",
|
128
|
+
"Account",
|
67
129
|
"Passenger",
|
68
130
|
"Reward",
|
69
131
|
"CreditCard",
|
70
132
|
"MeetGreetAdditional",
|
71
133
|
"Price",
|
72
134
|
"Reservation",
|
73
|
-
"
|
135
|
+
"EditReservationRequest",
|
74
136
|
"PriceRequest",
|
75
|
-
"PriceResponse",
|
76
137
|
"DetailsRequest",
|
77
|
-
"DetailsResponse",
|
78
138
|
"BookRequest",
|
79
|
-
"BookResponse",
|
80
139
|
"ListReservationsRequest",
|
81
|
-
"ListReservationsResponse",
|
82
140
|
"GetReservationRequest",
|
141
|
+
# Response models (serialize to snake_case for Python DX)
|
142
|
+
"BookResponse",
|
143
|
+
"CarClassPrice",
|
144
|
+
"DetailsResponse",
|
83
145
|
"EditReservationResponse",
|
146
|
+
"GetReservationResponse",
|
147
|
+
"ListReservationsResponse",
|
148
|
+
"PriceResponse",
|
149
|
+
# Explicit response model variants (for handling API responses)
|
150
|
+
"AccountResponse",
|
151
|
+
"AddressResponse",
|
152
|
+
"AirportResponse",
|
153
|
+
"BreakdownItemResponse",
|
154
|
+
"CityResponse",
|
155
|
+
"CreditCardResponse",
|
156
|
+
"ReservationData",
|
157
|
+
"LocationResponse",
|
158
|
+
"MeetGreetResponse",
|
159
|
+
"MeetGreetAdditionalResponse",
|
160
|
+
"PassengerResponse",
|
161
|
+
"ReservationResponse",
|
162
|
+
"RewardResponse",
|
163
|
+
"StopResponse",
|
164
|
+
# Places API schemas
|
84
165
|
"PlaceType",
|
85
166
|
"RankPreference",
|
86
167
|
"EVConnectorType",
|
@@ -99,5 +180,10 @@ __all__ = [
|
|
99
180
|
"AutocompletePlacesRequest",
|
100
181
|
"GeocodingRequest",
|
101
182
|
"SearchTextRequest",
|
183
|
+
"GooglePlace",
|
184
|
+
"PriceLevel",
|
185
|
+
"FieldMaskInput",
|
186
|
+
"BusinessStatus",
|
187
|
+
# Auth
|
102
188
|
"Credentials",
|
103
189
|
]
|
bookalimo/schemas/base.py
CHANGED
@@ -7,32 +7,78 @@ from enum import Enum
|
|
7
7
|
from typing import Any, cast
|
8
8
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, model_serializer
|
10
|
-
from pydantic.alias_generators import to_camel
|
10
|
+
from pydantic.alias_generators import to_camel, to_snake
|
11
11
|
from pydantic_core.core_schema import SerializationInfo, SerializerFunctionWrapHandler
|
12
12
|
|
13
13
|
|
14
|
-
|
14
|
+
def _deep_to_snake(obj: Any) -> Any:
|
15
|
+
if isinstance(obj, dict):
|
16
|
+
return {
|
17
|
+
(to_snake(k) if isinstance(k, str) else k): _deep_to_snake(v)
|
18
|
+
for k, v in obj.items()
|
19
|
+
}
|
20
|
+
if isinstance(obj, list):
|
21
|
+
return [_deep_to_snake(v) for v in obj]
|
22
|
+
if isinstance(obj, tuple):
|
23
|
+
return type(obj)(_deep_to_snake(v) for v in obj)
|
24
|
+
return obj
|
25
|
+
|
26
|
+
|
27
|
+
class SharedModel(BaseModel):
|
15
28
|
"""
|
16
|
-
Base model for
|
29
|
+
Base model for shared data structures used in both API requests and responses.
|
17
30
|
|
18
|
-
Provides
|
19
|
-
|
31
|
+
Provides field name conversion but no serialization opinions - subclasses decide
|
32
|
+
whether to serialize to camelCase (for requests) or snake_case (for responses).
|
20
33
|
"""
|
21
34
|
|
22
35
|
model_config = ConfigDict(
|
23
36
|
# Auto-generate camelCase aliases from snake_case field names
|
24
37
|
alias_generator=to_camel,
|
25
|
-
# Accept both alias (camel) and name (snake) on input
|
38
|
+
# Accept both alias (camel) and name (snake) on input
|
26
39
|
validate_by_alias=True,
|
27
|
-
validate_by_name=True,
|
28
|
-
#
|
29
|
-
serialize_by_alias=True,
|
40
|
+
validate_by_name=True,
|
41
|
+
# Don't set serialize_by_alias - let subclasses decide
|
30
42
|
# Enums dumping handled in model_serializer
|
31
43
|
use_enum_values=False,
|
32
44
|
# Ignore unknown keys from the API
|
33
45
|
extra="ignore",
|
34
46
|
)
|
35
47
|
|
48
|
+
|
49
|
+
class RequestModel(SharedModel):
|
50
|
+
"""
|
51
|
+
Base model for API request models.
|
52
|
+
|
53
|
+
Serializes to camelCase by default (what the Book-A-Limo API expects).
|
54
|
+
"""
|
55
|
+
|
56
|
+
model_config = ConfigDict(
|
57
|
+
alias_generator=to_camel,
|
58
|
+
validate_by_alias=True,
|
59
|
+
validate_by_name=True,
|
60
|
+
serialize_by_alias=True, # Serialize to camelCase for API requests
|
61
|
+
use_enum_values=False,
|
62
|
+
extra="ignore",
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
class ResponseModel(SharedModel):
|
67
|
+
"""
|
68
|
+
Base model for API response models.
|
69
|
+
|
70
|
+
Serializes to snake_case by default (better DX for Python developers).
|
71
|
+
"""
|
72
|
+
|
73
|
+
model_config = ConfigDict(
|
74
|
+
alias_generator=to_camel,
|
75
|
+
validate_by_alias=True,
|
76
|
+
validate_by_name=True,
|
77
|
+
serialize_by_alias=False, # Serialize to snake_case for Python developers
|
78
|
+
use_enum_values=False,
|
79
|
+
extra="ignore",
|
80
|
+
)
|
81
|
+
|
36
82
|
@model_serializer(mode="wrap")
|
37
83
|
def _serialize(
|
38
84
|
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
@@ -41,16 +87,30 @@ class ApiModel(BaseModel):
|
|
41
87
|
data = handler(self)
|
42
88
|
|
43
89
|
# Decide how to emit enums based on context (default to 'value')
|
44
|
-
|
90
|
+
ctx = info.context or {}
|
91
|
+
enum_out = ctx.get("enum_out", "value")
|
45
92
|
|
46
|
-
def
|
93
|
+
def convert_values(obj: Any) -> Any:
|
47
94
|
if isinstance(obj, Enum):
|
48
95
|
return obj.name if enum_out == "name" else obj.value
|
49
96
|
if isinstance(obj, dict):
|
50
|
-
return {k:
|
97
|
+
return {k: convert_values(v) for k, v in obj.items()}
|
51
98
|
if isinstance(obj, (list, tuple)):
|
52
99
|
t = type(obj)
|
53
|
-
return t(
|
100
|
+
return t(convert_values(v) for v in obj)
|
54
101
|
return obj
|
55
102
|
|
56
|
-
|
103
|
+
out = cast(dict[str, Any], convert_values(data))
|
104
|
+
|
105
|
+
# Key case control: default "camel" (uses aliases); allow "snake" via context.
|
106
|
+
case = ctx.get("case")
|
107
|
+
# Support boolean alias for convenience: snake_case=True -> case="snake"
|
108
|
+
if ctx.get("snake_case") is True:
|
109
|
+
case = "snake"
|
110
|
+
elif ctx.get("snake_case") is False:
|
111
|
+
case = "camel"
|
112
|
+
|
113
|
+
if case == "snake":
|
114
|
+
out = cast(dict[str, Any], _deep_to_snake(out))
|
115
|
+
|
116
|
+
return out
|
@@ -27,7 +27,7 @@ from .google import (
|
|
27
27
|
StructuredFormat,
|
28
28
|
Suggestion,
|
29
29
|
)
|
30
|
-
from .place import AddressComponent, GooglePlace
|
30
|
+
from .place import AddressComponent, BusinessStatus, GooglePlace, PriceLevel
|
31
31
|
|
32
32
|
__all__ = [
|
33
33
|
"PlaceType",
|
@@ -59,4 +59,6 @@ __all__ = [
|
|
59
59
|
"EVOptions",
|
60
60
|
"RoutingParameters",
|
61
61
|
"SearchTextResponse",
|
62
|
+
"PriceLevel",
|
63
|
+
"BusinessStatus",
|
62
64
|
]
|
@@ -210,12 +210,3 @@ class _Root:
|
|
210
210
|
|
211
211
|
|
212
212
|
F = _Root() # Usage: F.display_name, F.reviews.text, F.photos.author_attributions
|
213
|
-
|
214
|
-
|
215
|
-
if __name__ == "__main__":
|
216
|
-
print(
|
217
|
-
compile_field_mask(
|
218
|
-
"photos.author_attributions,photos.author_attributions.textt",
|
219
|
-
prefix="places",
|
220
|
-
)
|
221
|
-
)
|
@@ -126,8 +126,8 @@ class StructuredFormat(BaseModel):
|
|
126
126
|
|
127
127
|
model_config = ConfigDict(extra="forbid")
|
128
128
|
|
129
|
-
main_text: FormattableText
|
130
|
-
secondary_text: FormattableText
|
129
|
+
main_text: Optional[FormattableText] = None
|
130
|
+
secondary_text: Optional[FormattableText] = None
|
131
131
|
|
132
132
|
|
133
133
|
# ---------- Geometry Primitives ----------
|
@@ -651,37 +651,173 @@ class AutocompletePlacesRequest(BaseModel):
|
|
651
651
|
return self
|
652
652
|
|
653
653
|
|
654
|
+
class ExtraComputations(StrEnum):
|
655
|
+
"""Extra computations for Geocoding API requests."""
|
656
|
+
|
657
|
+
ADDRESS_DESCRIPTORS = "ADDRESS_DESCRIPTORS"
|
658
|
+
BUILDING_AND_ENTRANCES = "BUILDING_AND_ENTRANCES"
|
659
|
+
|
660
|
+
|
654
661
|
class GeocodingRequest(BaseModel):
|
655
662
|
"""
|
656
663
|
Pydantic model for validating and building Geocoding API query parameters.
|
664
|
+
Supports both forward geocoding (address -> coordinates) and reverse geocoding (coordinates -> address).
|
657
665
|
This model is not for a JSON request body, but for constructing a URL.
|
658
666
|
"""
|
659
667
|
|
668
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
669
|
+
|
670
|
+
# Forward geocoding parameters
|
660
671
|
address: Optional[str] = Field(
|
661
672
|
default=None,
|
662
673
|
description="The street address or plus code that you want to geocode.",
|
663
674
|
)
|
675
|
+
components: Optional[str] = Field(
|
676
|
+
default=None,
|
677
|
+
description="Components filter with pipe-separated component:value pairs (e.g., 'country:US|locality:Mountain View').",
|
678
|
+
)
|
679
|
+
|
680
|
+
# Reverse geocoding parameters
|
681
|
+
latlng: Optional[str] = Field(
|
682
|
+
default=None,
|
683
|
+
description="Latitude and longitude coordinates for reverse geocoding (e.g., '40.714224,-73.961452').",
|
684
|
+
)
|
664
685
|
place_id: Optional[str] = Field(
|
665
686
|
default=None,
|
666
687
|
description="The place ID of the place for which you wish to obtain the human-readable address.",
|
667
688
|
)
|
689
|
+
|
690
|
+
# Optional parameters for both forward and reverse geocoding
|
668
691
|
language: Optional[str] = Field(
|
669
|
-
default=None, description="The language in which to return results."
|
692
|
+
default=None, description="The language in which to return results (BCP-47)."
|
670
693
|
)
|
671
694
|
region: Optional[str] = Field(
|
672
695
|
default=None, description="The region code (ccTLD) to bias results."
|
673
696
|
)
|
697
|
+
extra_computations: list[ExtraComputations] = Field(
|
698
|
+
default_factory=list,
|
699
|
+
description="Additional features to include in the response.",
|
700
|
+
)
|
701
|
+
|
702
|
+
# Forward geocoding specific optional parameters
|
703
|
+
bounds: Optional[str] = Field(
|
704
|
+
default=None,
|
705
|
+
description="Bounding box for viewport biasing (format: 'southwest_lat,southwest_lng|northeast_lat,northeast_lng').",
|
706
|
+
)
|
707
|
+
|
708
|
+
# Reverse geocoding specific optional parameters
|
709
|
+
result_type: Optional[str] = Field(
|
710
|
+
default=None,
|
711
|
+
description="Filter for address types, pipe-separated (e.g., 'street_address|route').",
|
712
|
+
)
|
713
|
+
location_type: Optional[str] = Field(
|
714
|
+
default=None,
|
715
|
+
description="Filter for location types, pipe-separated (e.g., 'ROOFTOP|RANGE_INTERPOLATED').",
|
716
|
+
)
|
717
|
+
|
718
|
+
@field_validator("latlng")
|
719
|
+
@classmethod
|
720
|
+
def _validate_latlng(cls, v: Optional[str]) -> Optional[str]:
|
721
|
+
if v is None:
|
722
|
+
return v
|
723
|
+
try:
|
724
|
+
parts = v.split(",")
|
725
|
+
if len(parts) != 2:
|
726
|
+
raise ValueError("latlng must be in format 'latitude,longitude'")
|
727
|
+
lat, lng = float(parts[0]), float(parts[1])
|
728
|
+
if not (-90 <= lat <= 90):
|
729
|
+
raise ValueError("Latitude must be between -90 and 90")
|
730
|
+
if not (-180 <= lng <= 180):
|
731
|
+
raise ValueError("Longitude must be between -180 and 180")
|
732
|
+
except (ValueError, IndexError) as e:
|
733
|
+
if "could not convert" in str(e):
|
734
|
+
raise ValueError("latlng coordinates must be valid numbers") from None
|
735
|
+
raise
|
736
|
+
return v
|
737
|
+
|
738
|
+
@field_validator("bounds")
|
739
|
+
@classmethod
|
740
|
+
def _validate_bounds(cls, v: Optional[str]) -> Optional[str]:
|
741
|
+
if v is None:
|
742
|
+
return v
|
743
|
+
try:
|
744
|
+
parts = v.split("|")
|
745
|
+
if len(parts) != 2:
|
746
|
+
raise ValueError(
|
747
|
+
"bounds must be in format 'sw_lat,sw_lng|ne_lat,ne_lng'"
|
748
|
+
)
|
749
|
+
for part in parts:
|
750
|
+
coords = part.split(",")
|
751
|
+
if len(coords) != 2:
|
752
|
+
raise ValueError("Each bounds coordinate pair must be 'lat,lng'")
|
753
|
+
lat, lng = float(coords[0]), float(coords[1])
|
754
|
+
if not (-90 <= lat <= 90):
|
755
|
+
raise ValueError("Latitude must be between -90 and 90")
|
756
|
+
if not (-180 <= lng <= 180):
|
757
|
+
raise ValueError("Longitude must be between -180 and 180")
|
758
|
+
except (ValueError, IndexError) as e:
|
759
|
+
if "could not convert" in str(e):
|
760
|
+
raise ValueError("bounds coordinates must be valid numbers") from None
|
761
|
+
raise
|
762
|
+
return v
|
763
|
+
|
764
|
+
@field_validator("language")
|
765
|
+
@classmethod
|
766
|
+
def _validate_language_code(cls, v: Optional[str]) -> Optional[str]:
|
767
|
+
if v is None:
|
768
|
+
return v
|
769
|
+
if not BCP47.match(v):
|
770
|
+
raise ValueError(
|
771
|
+
"language must be a valid BCP-47 tag (e.g., 'en', 'en-US', 'zh-Hant')."
|
772
|
+
)
|
773
|
+
return v
|
774
|
+
|
775
|
+
@field_validator("region")
|
776
|
+
@classmethod
|
777
|
+
def _validate_region_code(cls, v: Optional[str]) -> Optional[str]:
|
778
|
+
if v is None:
|
779
|
+
return v
|
780
|
+
v2 = v.upper()
|
781
|
+
if not CLDR_REGION_2.fullmatch(v2):
|
782
|
+
raise ValueError(
|
783
|
+
"region must be a two-letter ccTLD region code (e.g., 'US', 'GB')."
|
784
|
+
)
|
785
|
+
return v2
|
786
|
+
|
787
|
+
@field_validator("extra_computations")
|
788
|
+
@classmethod
|
789
|
+
def _validate_extra_computations(
|
790
|
+
cls, v: list[ExtraComputations]
|
791
|
+
) -> list[ExtraComputations]:
|
792
|
+
# Remove duplicates while preserving order
|
793
|
+
seen = set()
|
794
|
+
cleaned = []
|
795
|
+
for computation in v:
|
796
|
+
if computation not in seen:
|
797
|
+
cleaned.append(computation)
|
798
|
+
seen.add(computation)
|
799
|
+
return cleaned
|
674
800
|
|
675
801
|
@model_validator(mode="before")
|
676
802
|
@classmethod
|
677
803
|
def check_required_params(cls, data: Any) -> Any:
|
678
|
-
"""
|
804
|
+
"""Validates that proper parameters are provided for forward or reverse geocoding."""
|
679
805
|
if isinstance(data, dict):
|
680
|
-
|
681
|
-
|
682
|
-
)
|
806
|
+
has_forward = any([data.get("address"), data.get("components")])
|
807
|
+
has_reverse = data.get("latlng") is not None
|
808
|
+
has_place_id = data.get("place_id") is not None
|
809
|
+
|
810
|
+
if not (has_forward or has_reverse or has_place_id):
|
811
|
+
raise ValueError(
|
812
|
+
"For forward geocoding: specify 'address' and/or 'components'. "
|
813
|
+
"For reverse geocoding: specify 'latlng'. "
|
814
|
+
"For place ID lookup: specify 'place_id'."
|
815
|
+
)
|
816
|
+
|
817
|
+
# Can't mix forward and reverse geocoding parameters
|
818
|
+
if has_reverse and (has_forward or has_place_id):
|
683
819
|
raise ValueError(
|
684
|
-
"
|
820
|
+
"Cannot mix reverse geocoding (latlng) with forward geocoding (address/components) or place_id lookup."
|
685
821
|
)
|
686
822
|
return data
|
687
823
|
|
@@ -690,18 +826,37 @@ class GeocodingRequest(BaseModel):
|
|
690
826
|
Serializes the model fields into a dictionary suitable for URL query parameters.
|
691
827
|
"""
|
692
828
|
params = QueryParams()
|
829
|
+
|
830
|
+
# Forward geocoding parameters
|
693
831
|
if self.address:
|
694
832
|
params = params.add("address", self.address)
|
695
|
-
|
833
|
+
if self.components:
|
834
|
+
params = params.add("components", self.components)
|
835
|
+
if self.bounds:
|
836
|
+
params = params.add("bounds", self.bounds)
|
837
|
+
|
838
|
+
# Reverse geocoding parameters
|
839
|
+
if self.latlng:
|
840
|
+
params = params.add("latlng", self.latlng)
|
841
|
+
if self.result_type:
|
842
|
+
params = params.add("result_type", self.result_type)
|
843
|
+
if self.location_type:
|
844
|
+
params = params.add("location_type", self.location_type)
|
845
|
+
|
846
|
+
# Place ID lookup
|
696
847
|
if self.place_id:
|
697
848
|
params = params.add("place_id", self.place_id)
|
698
849
|
|
850
|
+
# Common optional parameters
|
699
851
|
if self.language:
|
700
852
|
params = params.add("language", self.language)
|
701
|
-
|
702
853
|
if self.region:
|
703
854
|
params = params.add("region", self.region)
|
704
855
|
|
856
|
+
# Extra computations (can appear multiple times)
|
857
|
+
for computation in self.extra_computations:
|
858
|
+
params = params.add("extra_computations", computation.value)
|
859
|
+
|
705
860
|
return params
|
706
861
|
|
707
862
|
|