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.
@@ -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
- "EditableReservationRequest",
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
- 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
@@ -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
  ]
@@ -48,7 +48,7 @@ class LocalizedText(BaseModel):
48
48
  model_config = ConfigDict(extra="allow")
49
49
 
50
50
  text: str
51
- language_code: str
51
+ language_code: Optional[str] = None
52
52
 
53
53
  @field_validator("language_code")
54
54
  @classmethod
@@ -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
- """Ensures that either 'address', 'place_id', or 'components' is provided."""
804
+ """Validates that proper parameters are provided for forward or reverse geocoding."""
679
805
  if isinstance(data, dict):
680
- if not any(
681
- [data.get("address"), data.get("place_id"), data.get("components")]
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
- "You must specify either 'address', 'place_id', or 'components'."
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