bookalimo 0.1.5__py3-none-any.whl → 1.0.0__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/__init__.py +17 -24
- bookalimo/_version.py +9 -0
- bookalimo/client.py +310 -0
- bookalimo/config.py +16 -0
- bookalimo/exceptions.py +115 -5
- bookalimo/integrations/__init__.py +1 -0
- bookalimo/integrations/google_places/__init__.py +31 -0
- bookalimo/integrations/google_places/client_async.py +258 -0
- bookalimo/integrations/google_places/client_sync.py +257 -0
- bookalimo/integrations/google_places/common.py +245 -0
- bookalimo/integrations/google_places/proto_adapter.py +224 -0
- bookalimo/{_logging.py → logging.py} +45 -42
- bookalimo/schemas/__init__.py +97 -0
- bookalimo/schemas/base.py +56 -0
- bookalimo/{models.py → schemas/booking.py} +88 -100
- bookalimo/schemas/places/__init__.py +37 -0
- bookalimo/schemas/places/common.py +198 -0
- bookalimo/schemas/places/google.py +596 -0
- bookalimo/schemas/places/place.py +337 -0
- bookalimo/services/__init__.py +11 -0
- bookalimo/services/pricing.py +191 -0
- bookalimo/services/reservations.py +227 -0
- bookalimo/transport/__init__.py +7 -0
- bookalimo/transport/auth.py +41 -0
- bookalimo/transport/base.py +44 -0
- bookalimo/transport/httpx_async.py +230 -0
- bookalimo/transport/httpx_sync.py +230 -0
- bookalimo/transport/retry.py +102 -0
- bookalimo/transport/utils.py +59 -0
- bookalimo-1.0.0.dist-info/METADATA +307 -0
- bookalimo-1.0.0.dist-info/RECORD +35 -0
- bookalimo/_client.py +0 -420
- bookalimo/wrapper.py +0 -444
- bookalimo-0.1.5.dist-info/METADATA +0 -392
- bookalimo-0.1.5.dist-info/RECORD +0 -12
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/WHEEL +0 -0
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {bookalimo-0.1.5.dist-info → bookalimo-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from pydantic import (
|
7
|
+
AnyUrl,
|
8
|
+
BaseModel,
|
9
|
+
ConfigDict,
|
10
|
+
Field,
|
11
|
+
field_validator,
|
12
|
+
model_validator,
|
13
|
+
)
|
14
|
+
from typing_extensions import Self
|
15
|
+
|
16
|
+
BCP47 = re.compile(
|
17
|
+
r"^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$"
|
18
|
+
) # pragmatic, e.g., "en", "en-US", "zh-Hant"
|
19
|
+
|
20
|
+
CLDR_REGION_2 = re.compile(r"^[A-Z]{2}$") # e.g., "US", "GB"
|
21
|
+
|
22
|
+
PLACE_ID = re.compile(
|
23
|
+
r"^[A-Za-z0-9_-]{10,}$"
|
24
|
+
) # pragmatic: real IDs are long base64url-ish e.g., "ChIJN1t_tDeuEmsRUsoyG83frY4"
|
25
|
+
|
26
|
+
PLACE_RESOURCE = re.compile(
|
27
|
+
r"^places/[A-Za-z0-9_-]{10,}$"
|
28
|
+
) # e.g., "places/ChIJN1t_tDeuEmsRUsoyG83frY4"
|
29
|
+
|
30
|
+
PLACE_TYPE = re.compile(r"^[a-z0-9_]+$") # e.g., "gas_station", "restaurant"
|
31
|
+
|
32
|
+
HEX_COLOR = re.compile(r"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$") # #abc, #a1b2c3
|
33
|
+
|
34
|
+
PLUS_GLOBAL = re.compile(
|
35
|
+
r"^[23456789CFGHJMPQRVWX]{4,8}\+[23456789CFGHJMPQRVWX]{2,}$"
|
36
|
+
) # Open Location Code (Plus Code), pragmatic
|
37
|
+
|
38
|
+
BASE64URL_36 = re.compile(
|
39
|
+
r"^[A-Za-z0-9_-]{1,36}$"
|
40
|
+
) # up to 36 chars, URL-safe base64-ish
|
41
|
+
|
42
|
+
|
43
|
+
# ---------- “External” Google message wrappers ----------
|
44
|
+
class ExternalModel(BaseModel):
|
45
|
+
"""Permissive wrapper for Google messages we don't model in detail."""
|
46
|
+
|
47
|
+
model_config = ConfigDict(extra="allow")
|
48
|
+
|
49
|
+
|
50
|
+
class OpeningHours(ExternalModel): ...
|
51
|
+
|
52
|
+
|
53
|
+
class PostalAddress(ExternalModel): ...
|
54
|
+
|
55
|
+
|
56
|
+
class TimeZone(ExternalModel): ...
|
57
|
+
|
58
|
+
|
59
|
+
class Timestamp(ExternalModel): ...
|
60
|
+
|
61
|
+
|
62
|
+
class Date(ExternalModel): ...
|
63
|
+
|
64
|
+
|
65
|
+
class ContentBlock(ExternalModel): ...
|
66
|
+
|
67
|
+
|
68
|
+
class Photo(ExternalModel): ...
|
69
|
+
|
70
|
+
|
71
|
+
class Review(ExternalModel): ...
|
72
|
+
|
73
|
+
|
74
|
+
class FuelOptions(ExternalModel): ...
|
75
|
+
|
76
|
+
|
77
|
+
class EVChargeOptions(ExternalModel): ...
|
78
|
+
|
79
|
+
|
80
|
+
class AddressDescriptor(ExternalModel): ...
|
81
|
+
|
82
|
+
|
83
|
+
class PriceRange(ExternalModel): ...
|
84
|
+
|
85
|
+
|
86
|
+
# ---------- Geometry ----------
|
87
|
+
class LatLng(BaseModel):
|
88
|
+
model_config = ConfigDict(extra="forbid")
|
89
|
+
|
90
|
+
latitude: float = Field(..., description="[-90, 90]")
|
91
|
+
longitude: float = Field(..., description="[-180, 180]")
|
92
|
+
|
93
|
+
@model_validator(mode="after")
|
94
|
+
def _check(self) -> Self:
|
95
|
+
if not (-90 <= self.latitude <= 90):
|
96
|
+
raise ValueError("latitude must be between -90 and 90")
|
97
|
+
if not (-180 <= self.longitude <= 180):
|
98
|
+
raise ValueError("longitude must be between -180 and 180")
|
99
|
+
return self
|
100
|
+
|
101
|
+
|
102
|
+
class Viewport(BaseModel):
|
103
|
+
model_config = ConfigDict(extra="forbid")
|
104
|
+
|
105
|
+
high: LatLng
|
106
|
+
low: LatLng
|
107
|
+
|
108
|
+
@model_validator(mode="after")
|
109
|
+
def _lat_order(self) -> Self:
|
110
|
+
if self.high.latitude < self.low.latitude:
|
111
|
+
raise ValueError("high.latitude must be >= low.latitude")
|
112
|
+
return self
|
113
|
+
|
114
|
+
|
115
|
+
# ---------- Simple shared types ----------
|
116
|
+
class PlusCode(BaseModel):
|
117
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
118
|
+
|
119
|
+
global_code: Optional[str] = Field(default=None, description="e.g., '9FWM33GV+HQ'")
|
120
|
+
compound_code: Optional[str] = Field(default=None)
|
121
|
+
|
122
|
+
@field_validator("global_code")
|
123
|
+
@classmethod
|
124
|
+
def _check_global(cls, v: Optional[str]) -> Optional[str]:
|
125
|
+
if v is None:
|
126
|
+
return v
|
127
|
+
# Accept common formats; be permissive.
|
128
|
+
if not PLUS_GLOBAL.match(v):
|
129
|
+
# Let a variety of valid codes through without being too strict.
|
130
|
+
if "+" not in v or len(v) < 6:
|
131
|
+
raise ValueError(
|
132
|
+
"global_code must look like a valid plus code (e.g., '9FWM33GV+HQ')."
|
133
|
+
)
|
134
|
+
return v
|
135
|
+
|
136
|
+
|
137
|
+
class Attribution(BaseModel):
|
138
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
139
|
+
|
140
|
+
provider: str
|
141
|
+
provider_uri: Optional[AnyUrl] = None
|
142
|
+
|
143
|
+
|
144
|
+
class SubDestination(BaseModel):
|
145
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
146
|
+
|
147
|
+
name: str = Field(..., description="places/{place_id}")
|
148
|
+
id: str = Field(..., description="{place_id}")
|
149
|
+
|
150
|
+
@field_validator("name")
|
151
|
+
@classmethod
|
152
|
+
def _name(cls, v: str) -> str:
|
153
|
+
if not PLACE_RESOURCE.fullmatch(v):
|
154
|
+
raise ValueError("name must be in the form 'places/{place_id}'")
|
155
|
+
return v
|
156
|
+
|
157
|
+
@field_validator("id")
|
158
|
+
@classmethod
|
159
|
+
def _id(cls, v: str) -> str:
|
160
|
+
if not PLACE_ID.fullmatch(v):
|
161
|
+
raise ValueError("id must be a base64url-like token (>=10 chars)")
|
162
|
+
return v
|
163
|
+
|
164
|
+
@model_validator(mode="after")
|
165
|
+
def _match(self) -> Self:
|
166
|
+
if self.name.split("/", 1)[1] != self.id:
|
167
|
+
raise ValueError("id must match the trailing component of name")
|
168
|
+
return self
|
169
|
+
|
170
|
+
|
171
|
+
class AccessibilityOptions(BaseModel):
|
172
|
+
model_config = ConfigDict(extra="forbid")
|
173
|
+
|
174
|
+
wheelchair_accessible_parking: Optional[bool] = None
|
175
|
+
wheelchair_accessible_entrance: Optional[bool] = None
|
176
|
+
wheelchair_accessible_restroom: Optional[bool] = None
|
177
|
+
wheelchair_accessible_seating: Optional[bool] = None
|
178
|
+
|
179
|
+
|
180
|
+
class PaymentOptions(BaseModel):
|
181
|
+
model_config = ConfigDict(extra="forbid")
|
182
|
+
|
183
|
+
accepts_credit_cards: Optional[bool] = None
|
184
|
+
accepts_debit_cards: Optional[bool] = None
|
185
|
+
accepts_cash_only: Optional[bool] = None
|
186
|
+
accepts_nfc: Optional[bool] = None
|
187
|
+
|
188
|
+
|
189
|
+
class ParkingOptions(BaseModel):
|
190
|
+
model_config = ConfigDict(extra="forbid")
|
191
|
+
|
192
|
+
free_parking_lot: Optional[bool] = None
|
193
|
+
paid_parking_lot: Optional[bool] = None
|
194
|
+
free_street_parking: Optional[bool] = None
|
195
|
+
paid_street_parking: Optional[bool] = None
|
196
|
+
valet_parking: Optional[bool] = None
|
197
|
+
free_garage_parking: Optional[bool] = None
|
198
|
+
paid_garage_parking: Optional[bool] = None
|