bookalimo 1.0.0__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.
@@ -1,84 +1,170 @@
1
1
  """Pydantic schemas for the Bookalimo SDK."""
2
2
 
3
3
  from ..transport.auth import Credentials
4
- from .booking import (
4
+ from .places import (
5
+ AutocompletePlacesRequest,
6
+ AutocompletePlacesResponse,
7
+ BusinessStatus,
8
+ Circle,
9
+ EVConnectorType,
10
+ FieldMaskInput,
11
+ FormattableText,
12
+ GeocodingRequest,
13
+ GetPlaceRequest,
14
+ GooglePlace,
15
+ LocationBias,
16
+ LocationRestriction,
17
+ Place,
18
+ PlacePrediction,
19
+ PlaceType,
20
+ PriceLevel,
21
+ QueryPrediction,
22
+ RankPreference,
23
+ SearchTextRequest,
24
+ StringRange,
25
+ StructuredFormat,
26
+ Suggestion,
27
+ )
28
+
29
+ # Import request models (for API calls - serialize to camelCase)
30
+ from .requests import (
31
+ Account,
5
32
  Address,
6
33
  Airport,
7
34
  BookRequest,
8
- BookResponse,
9
- CardHolderType,
10
35
  City,
11
36
  CreditCard,
12
37
  DetailsRequest,
13
- DetailsResponse,
14
- EditableReservationRequest,
15
- EditReservationResponse,
38
+ EditReservationRequest,
16
39
  GetReservationRequest,
17
40
  ListReservationsRequest,
18
- ListReservationsResponse,
19
41
  Location,
20
- LocationType,
21
42
  MeetGreetAdditional,
22
- MeetGreetType,
23
43
  Passenger,
24
44
  Price,
25
45
  PriceRequest,
26
- PriceResponse,
27
- RateType,
28
46
  Reservation,
29
- ReservationStatus,
30
47
  Reward,
31
- RewardType,
32
48
  Stop,
33
49
  )
34
- from .places import (
35
- AutocompletePlacesRequest,
36
- AutocompletePlacesResponse,
37
- Circle,
38
- FormattableText,
39
- GeocodingRequest,
40
- GetPlaceRequest,
41
- LocationBias,
42
- LocationRestriction,
43
- Place,
44
- PlacePrediction,
45
- PlaceType,
46
- QueryPrediction,
47
- StringRange,
48
- StructuredFormat,
49
- Suggestion,
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,
50
112
  )
51
113
 
52
114
  __all__ = [
115
+ # Enums and shared types
53
116
  "RateType",
54
117
  "LocationType",
55
118
  "MeetGreetType",
56
119
  "RewardType",
57
120
  "ReservationStatus",
58
121
  "CardHolderType",
122
+ # Request models (default exports - serialize to camelCase for API)
59
123
  "City",
60
124
  "Address",
61
125
  "Airport",
62
126
  "Location",
63
127
  "Stop",
128
+ "Account",
64
129
  "Passenger",
65
130
  "Reward",
66
131
  "CreditCard",
67
132
  "MeetGreetAdditional",
68
133
  "Price",
69
134
  "Reservation",
70
- "EditableReservationRequest",
135
+ "EditReservationRequest",
71
136
  "PriceRequest",
72
- "PriceResponse",
73
137
  "DetailsRequest",
74
- "DetailsResponse",
75
138
  "BookRequest",
76
- "BookResponse",
77
139
  "ListReservationsRequest",
78
- "ListReservationsResponse",
79
140
  "GetReservationRequest",
141
+ # Response models (serialize to snake_case for Python DX)
142
+ "BookResponse",
143
+ "CarClassPrice",
144
+ "DetailsResponse",
80
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
81
165
  "PlaceType",
166
+ "RankPreference",
167
+ "EVConnectorType",
82
168
  "StringRange",
83
169
  "FormattableText",
84
170
  "StructuredFormat",
@@ -93,5 +179,11 @@ __all__ = [
93
179
  "GetPlaceRequest",
94
180
  "AutocompletePlacesRequest",
95
181
  "GeocodingRequest",
182
+ "SearchTextRequest",
183
+ "GooglePlace",
184
+ "PriceLevel",
185
+ "FieldMaskInput",
186
+ "BusinessStatus",
187
+ # Auth
96
188
  "Credentials",
97
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
- class ApiModel(BaseModel):
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 all Book-A-Limo API models.
29
+ Base model for shared data structures used in both API requests and responses.
17
30
 
18
- Provides automatic field name conversion between Python snake_case
19
- and API camelCase, plus proper enum handling.
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 (v2.11+)
38
+ # Accept both alias (camel) and name (snake) on input
26
39
  validate_by_alias=True,
27
- validate_by_name=True, # replaces populate_by_name
28
- # Default to using aliases when dumping (wire format)
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
- enum_out = (info.context or {}).get("enum_out", "value")
90
+ ctx = info.context or {}
91
+ enum_out = ctx.get("enum_out", "value")
45
92
 
46
- def convert(obj: Any) -> Any:
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: convert(v) for k, v in obj.items()}
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(convert(v) for v in obj)
100
+ return t(convert_values(v) for v in obj)
54
101
  return obj
55
102
 
56
- return cast(dict[str, Any], convert(data))
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
@@ -1,9 +1,13 @@
1
1
  """Google Places API schemas."""
2
2
 
3
+ from .common import AddressDescriptor
4
+ from .field_mask import FieldMaskInput, FieldPath, compile_field_mask
3
5
  from .google import (
4
6
  AutocompletePlacesRequest,
5
7
  AutocompletePlacesResponse,
6
8
  Circle,
9
+ EVConnectorType,
10
+ EVOptions,
7
11
  FormattableText,
8
12
  GeocodingRequest,
9
13
  GetPlaceRequest,
@@ -13,20 +17,31 @@ from .google import (
13
17
  PlacePrediction,
14
18
  PlaceType,
15
19
  QueryPrediction,
20
+ RankPreference,
21
+ ResolvedAirport,
22
+ RoutingParameters,
23
+ SearchTextLocationRestriction,
24
+ SearchTextRequest,
25
+ SearchTextResponse,
16
26
  StringRange,
17
27
  StructuredFormat,
18
28
  Suggestion,
19
29
  )
30
+ from .place import AddressComponent, BusinessStatus, GooglePlace, PriceLevel
20
31
 
21
32
  __all__ = [
22
33
  "PlaceType",
34
+ "RankPreference",
35
+ "EVConnectorType",
23
36
  "StringRange",
24
37
  "FormattableText",
25
38
  "StructuredFormat",
26
39
  "Circle",
27
40
  "LocationBias",
28
41
  "LocationRestriction",
42
+ "SearchTextLocationRestriction",
29
43
  "Place",
44
+ "GooglePlace",
30
45
  "AutocompletePlacesResponse",
31
46
  "PlacePrediction",
32
47
  "QueryPrediction",
@@ -34,4 +49,16 @@ __all__ = [
34
49
  "GetPlaceRequest",
35
50
  "AutocompletePlacesRequest",
36
51
  "GeocodingRequest",
52
+ "SearchTextRequest",
53
+ "ResolvedAirport",
54
+ "AddressDescriptor",
55
+ "AddressComponent",
56
+ "FieldMaskInput",
57
+ "FieldPath",
58
+ "compile_field_mask",
59
+ "EVOptions",
60
+ "RoutingParameters",
61
+ "SearchTextResponse",
62
+ "PriceLevel",
63
+ "BusinessStatus",
37
64
  ]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ from enum import IntEnum
4
5
  from typing import Optional
5
6
 
6
7
  from pydantic import (
@@ -40,7 +41,24 @@ BASE64URL_36 = re.compile(
40
41
  ) # up to 36 chars, URL-safe base64-ish
41
42
 
42
43
 
43
- # ---------- “External” Google message wrappers ----------
44
+ # ---------- Fundamental types ----------
45
+ class LocalizedText(BaseModel):
46
+ """Localized text with language code."""
47
+
48
+ model_config = ConfigDict(extra="allow")
49
+
50
+ text: str
51
+ language_code: Optional[str] = None
52
+
53
+ @field_validator("language_code")
54
+ @classmethod
55
+ def _language_code(cls, v: str) -> str:
56
+ if not BCP47.match(v):
57
+ raise ValueError("language_code must be a valid BCP-47 language tag")
58
+ return v
59
+
60
+
61
+ # ---------- "External" Google message wrappers ----------
44
62
  class ExternalModel(BaseModel):
45
63
  """Permissive wrapper for Google messages we don't model in detail."""
46
64
 
@@ -77,7 +95,142 @@ class FuelOptions(ExternalModel): ...
77
95
  class EVChargeOptions(ExternalModel): ...
78
96
 
79
97
 
80
- class AddressDescriptor(ExternalModel): ...
98
+ class AddressDescriptor(BaseModel):
99
+ """A relational description of a location with nearby landmarks and containing areas."""
100
+
101
+ model_config = ConfigDict(extra="forbid")
102
+
103
+ class Landmark(BaseModel):
104
+ """Basic landmark information and relationship with target location."""
105
+
106
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
107
+
108
+ class SpatialRelationship(IntEnum):
109
+ """Spatial relationship between target location and landmark."""
110
+
111
+ NEAR = 0
112
+ WITHIN = 1
113
+ BESIDE = 2
114
+ ACROSS_THE_ROAD = 3
115
+ DOWN_THE_ROAD = 4
116
+ AROUND_THE_CORNER = 5
117
+ BEHIND = 6
118
+
119
+ name: str = Field(..., description="Landmark's resource name")
120
+ place_id: str = Field(..., description="Landmark's place ID")
121
+ display_name: LocalizedText = Field(..., description="Landmark's display name")
122
+ types: list[str] = Field(
123
+ default_factory=list, description="Type tags for landmark"
124
+ )
125
+ spatial_relationship: SpatialRelationship = Field(
126
+ default=SpatialRelationship.NEAR,
127
+ description="Spatial relationship to target",
128
+ )
129
+ straight_line_distance_meters: float = Field(
130
+ ..., ge=0.0, description="Straight line distance in meters"
131
+ )
132
+ travel_distance_meters: Optional[float] = Field(
133
+ default=None,
134
+ ge=0.0,
135
+ description="Travel distance in meters along road network",
136
+ )
137
+
138
+ @field_validator("name")
139
+ @classmethod
140
+ def _name(cls, v: str) -> str:
141
+ if not PLACE_RESOURCE.fullmatch(v):
142
+ raise ValueError("name must be in the form 'places/{place_id}'")
143
+ return v
144
+
145
+ @field_validator("place_id")
146
+ @classmethod
147
+ def _place_id(cls, v: str) -> str:
148
+ if not PLACE_ID.fullmatch(v):
149
+ raise ValueError("place_id must be a valid Place ID")
150
+ return v
151
+
152
+ @field_validator("types")
153
+ @classmethod
154
+ def _types(cls, v: list[str]) -> list[str]:
155
+ out, seen = [], set()
156
+ for raw in v:
157
+ t = raw.strip()
158
+ if not t:
159
+ raise ValueError("types cannot contain empty strings")
160
+ if not PLACE_TYPE.fullmatch(t):
161
+ raise ValueError(f"invalid place type '{t}'")
162
+ if t not in seen:
163
+ out.append(t)
164
+ seen.add(t)
165
+ return out
166
+
167
+ @model_validator(mode="after")
168
+ def _name_id_consistency(self) -> Self:
169
+ if self.name.split("/", 1)[1] != self.place_id:
170
+ raise ValueError("place_id must match the trailing component of name")
171
+ return self
172
+
173
+ class Area(BaseModel):
174
+ """Area information and relationship with target location."""
175
+
176
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
177
+
178
+ class Containment(IntEnum):
179
+ """Spatial relationship between target location and area."""
180
+
181
+ CONTAINMENT_UNSPECIFIED = 0
182
+ WITHIN = 1
183
+ OUTSKIRTS = 2
184
+ NEAR = 3
185
+
186
+ name: str = Field(..., description="Area's resource name")
187
+ place_id: str = Field(..., description="Area's place ID")
188
+ display_name: LocalizedText = Field(..., description="Area's display name")
189
+ containment: Containment = Field(
190
+ default=Containment.CONTAINMENT_UNSPECIFIED,
191
+ description="Spatial relationship to target",
192
+ )
193
+
194
+ @field_validator("name")
195
+ @classmethod
196
+ def _name(cls, v: str) -> str:
197
+ if not PLACE_RESOURCE.fullmatch(v):
198
+ raise ValueError("name must be in the form 'places/{place_id}'")
199
+ return v
200
+
201
+ @field_validator("place_id")
202
+ @classmethod
203
+ def _place_id(cls, v: str) -> str:
204
+ if not PLACE_ID.fullmatch(v):
205
+ raise ValueError("place_id must be a valid Place ID")
206
+ return v
207
+
208
+ @model_validator(mode="after")
209
+ def _name_id_consistency(self) -> Self:
210
+ if self.name.split("/", 1)[1] != self.place_id:
211
+ raise ValueError("place_id must match the trailing component of name")
212
+ return self
213
+
214
+ landmarks: list[Landmark] = Field(
215
+ default_factory=list, description="Ranked list of nearby landmarks"
216
+ )
217
+ areas: list[Area] = Field(
218
+ default_factory=list, description="Ranked list of containing or adjacent areas"
219
+ )
220
+
221
+ @field_validator("landmarks")
222
+ @classmethod
223
+ def _max_landmarks(cls, v: list[Landmark]) -> list[Landmark]:
224
+ if len(v) > 10: # Reasonable limit for API responses
225
+ raise ValueError("landmarks can contain at most 10 items")
226
+ return v
227
+
228
+ @field_validator("areas")
229
+ @classmethod
230
+ def _max_areas(cls, v: list[Area]) -> list[Area]:
231
+ if len(v) > 10: # Reasonable limit for API responses
232
+ raise ValueError("areas can contain at most 10 items")
233
+ return v
81
234
 
82
235
 
83
236
  class PriceRange(ExternalModel): ...