bookalimo 0.1.5__py3-none-any.whl → 1.0.1__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.
Files changed (42) hide show
  1. bookalimo/__init__.py +17 -24
  2. bookalimo/_version.py +9 -0
  3. bookalimo/client.py +310 -0
  4. bookalimo/config.py +16 -0
  5. bookalimo/exceptions.py +115 -5
  6. bookalimo/integrations/__init__.py +1 -0
  7. bookalimo/integrations/google_places/__init__.py +31 -0
  8. bookalimo/integrations/google_places/client_async.py +289 -0
  9. bookalimo/integrations/google_places/client_sync.py +287 -0
  10. bookalimo/integrations/google_places/common.py +231 -0
  11. bookalimo/integrations/google_places/proto_adapter.py +224 -0
  12. bookalimo/integrations/google_places/resolve_airport.py +397 -0
  13. bookalimo/integrations/google_places/transports.py +98 -0
  14. bookalimo/{_logging.py → logging.py} +45 -42
  15. bookalimo/schemas/__init__.py +103 -0
  16. bookalimo/schemas/base.py +56 -0
  17. bookalimo/{models.py → schemas/booking.py} +88 -100
  18. bookalimo/schemas/places/__init__.py +62 -0
  19. bookalimo/schemas/places/common.py +351 -0
  20. bookalimo/schemas/places/field_mask.py +221 -0
  21. bookalimo/schemas/places/google.py +883 -0
  22. bookalimo/schemas/places/place.py +334 -0
  23. bookalimo/services/__init__.py +11 -0
  24. bookalimo/services/pricing.py +191 -0
  25. bookalimo/services/reservations.py +227 -0
  26. bookalimo/transport/__init__.py +7 -0
  27. bookalimo/transport/auth.py +41 -0
  28. bookalimo/transport/base.py +44 -0
  29. bookalimo/transport/httpx_async.py +230 -0
  30. bookalimo/transport/httpx_sync.py +230 -0
  31. bookalimo/transport/retry.py +102 -0
  32. bookalimo/transport/utils.py +59 -0
  33. bookalimo-1.0.1.dist-info/METADATA +370 -0
  34. bookalimo-1.0.1.dist-info/RECORD +38 -0
  35. bookalimo-1.0.1.dist-info/licenses/LICENSE +21 -0
  36. bookalimo/_client.py +0 -420
  37. bookalimo/wrapper.py +0 -444
  38. bookalimo-0.1.5.dist-info/METADATA +0 -392
  39. bookalimo-0.1.5.dist-info/RECORD +0 -12
  40. bookalimo-0.1.5.dist-info/licenses/LICENSE +0 -0
  41. {bookalimo-0.1.5.dist-info → bookalimo-1.0.1.dist-info}/WHEEL +0 -0
  42. {bookalimo-0.1.5.dist-info → bookalimo-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,397 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import math
5
+ import os
6
+ import re
7
+ import unicodedata
8
+ from functools import lru_cache
9
+ from importlib.resources import files
10
+ from typing import Any, Optional, cast
11
+
12
+ import numpy as np
13
+ from numpy.typing import NDArray
14
+ from rapidfuzz import fuzz, process
15
+
16
+ from bookalimo.schemas.places import GooglePlace, ResolvedAirport
17
+
18
+ # ---------- Config ----------
19
+ CSV_PATH = os.environ.get(
20
+ "AIRPORTS_CSV", str(files("airportsdata").joinpath("airports.csv"))
21
+ )
22
+ DEFAULT_MAX_RESULTS = 20 # number of airports to return
23
+ DIST_KM_SCALE = 200.0 # distance scale for proximity confidence
24
+
25
+ # Google types that clearly indicate “airport-ish” places
26
+ AIRPORTY_TYPES = {
27
+ "airport",
28
+ "international_airport",
29
+ "airstrip",
30
+ "heliport",
31
+ }
32
+
33
+ # Small bonus when a candidate airport’s IATA/ICAO matches codes hinted by Places
34
+ CODE_BONUS_QUERY = 15.0 # user typed a code (strong)
35
+ CODE_BONUS_PLACES = 8.0 # code inferred from Places strings (softer)
36
+
37
+
38
+ # ---------- Helpers ----------
39
+ def _norm(s: Optional[str]) -> str:
40
+ if not s:
41
+ return ""
42
+ s = unicodedata.normalize("NFKD", s)
43
+ s = "".join(ch for ch in s if not unicodedata.combining(ch))
44
+ s = s.lower()
45
+ s = re.sub(r"[^a-z0-9]+", " ", s).strip()
46
+ return s
47
+
48
+
49
+ def _haversine_km_scalar_to_many(
50
+ lat1_rad: float,
51
+ lon1_rad: float,
52
+ lat2_rad: NDArray[np.float64],
53
+ lon2_rad: NDArray[np.float64],
54
+ ) -> NDArray[np.float64]:
55
+ dlat = lat2_rad - lat1_rad
56
+ dlon = lon2_rad - lon1_rad
57
+ a = (
58
+ np.sin(dlat / 2.0) ** 2
59
+ + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon / 2.0) ** 2
60
+ )
61
+ c: NDArray[np.float64] = 2.0 * np.arcsin(np.sqrt(a))
62
+ return cast(NDArray[np.float64], 6371.0088 * c) # mean Earth radius (km)
63
+
64
+
65
+ def _looks_like_code(q: str) -> tuple[Optional[str], Optional[str]]:
66
+ q = q.strip().upper()
67
+ if re.fullmatch(r"[A-Z0-9]{3}", q):
68
+ return (q, None) # likely IATA
69
+ if re.fullmatch(r"[A-Z0-9]{4}", q):
70
+ return (None, q) # likely ICAO
71
+ return (None, None)
72
+
73
+
74
+ def _extract_codes_from_text(s: str) -> tuple[set[str], set[str]]:
75
+ """
76
+ Pull 3- or 4-char uppercase tokens that *could* be codes.
77
+ We'll only use these with a small bonus and only if the place looks airport-ish.
78
+ """
79
+ tokens = set(re.findall(r"\b[A-Z0-9]{3,4}\b", s.upper()))
80
+ iata = {t for t in tokens if re.fullmatch(r"[A-Z]{3}", t)}
81
+ icao = {t for t in tokens if re.fullmatch(r"[A-Z0-9]{4}", t)}
82
+ return iata, icao
83
+
84
+
85
+ def _place_points(places: list[GooglePlace]) -> list[tuple[float, float]]:
86
+ """
87
+ Extract (lat, lon) from Places responses. Prefers 'location', then viewport center,
88
+ then Plus Code (if the openlocationcode lib is available).
89
+ """
90
+ pts: list[tuple[float, float]] = []
91
+ for p in places or []:
92
+ # p.location (LatLng)
93
+ loc = getattr(p, "location", None)
94
+ if loc is not None and hasattr(loc, "latitude") and hasattr(loc, "longitude"):
95
+ pts.append((float(loc.latitude), float(loc.longitude)))
96
+ continue
97
+
98
+ # p.viewport (Viewport -> center)
99
+ vp = getattr(p, "viewport", None)
100
+ if vp is not None and hasattr(vp, "high") and hasattr(vp, "low"):
101
+ try:
102
+ lat = (float(vp.high.latitude) + float(vp.low.latitude)) / 2.0
103
+ lon = (float(vp.high.longitude) + float(vp.low.longitude)) / 2.0
104
+ pts.append((lat, lon))
105
+ continue
106
+ except Exception:
107
+ pass
108
+ return pts
109
+
110
+
111
+ def _place_hints_and_codes(
112
+ places: list[GooglePlace],
113
+ ) -> tuple[list[str], set[str], set[str]]:
114
+ """
115
+ Collect a few high-utility strings from Places to augment text matching,
116
+ plus soft code candidates (IATA/ICAO) extracted from those strings.
117
+ We prioritize places whose types include airport-ish categories.
118
+ """
119
+ hints_prioritized: list[str] = []
120
+ hints_general: list[str] = []
121
+ iata_cand: set[str] = set()
122
+ icao_cand: set[str] = set()
123
+
124
+ for p in places or []:
125
+ types = set(getattr(p, "types", []) or [])
126
+ primary = getattr(p, "primary_type", None) or ""
127
+ airporty = bool(types & AIRPORTY_TYPES) or (primary in AIRPORTY_TYPES)
128
+
129
+ # Display name & address
130
+ disp = getattr(p, "display_name", None)
131
+ disp_txt = getattr(disp, "text", None) if disp is not None else None
132
+ addr = getattr(p, "formatted_address", None)
133
+
134
+ # A few relational names (areas/landmarks)
135
+ adesc = getattr(p, "address_descriptor", None)
136
+ area_names = []
137
+ lm_names = []
138
+ if adesc is not None:
139
+ for a in (getattr(adesc, "areas", []) or [])[:2]:
140
+ dn = getattr(a, "display_name", None)
141
+ if dn and getattr(dn, "text", None):
142
+ area_names.append(dn.text)
143
+ for lm in (getattr(adesc, "landmarks", []) or [])[:2]:
144
+ dn = getattr(lm, "display_name", None)
145
+ if dn and getattr(dn, "text", None):
146
+ lm_names.append(dn.text)
147
+
148
+ # Gather hint candidates
149
+ candidates = [disp_txt, addr, *area_names, *lm_names]
150
+ candidates = [c for c in candidates if c]
151
+ if not candidates:
152
+ continue
153
+
154
+ # Extract soft code candidates from the most descriptive strings
155
+ for s in candidates[:2]:
156
+ i3, i4 = _extract_codes_from_text(s)
157
+ if airporty:
158
+ iata_cand |= i3
159
+ icao_cand |= i4
160
+
161
+ # Prioritize hints if the place is airport-ish
162
+ (hints_prioritized if airporty else hints_general).extend(candidates[:2])
163
+
164
+ # De-dup (by normalized form) and cap to keep RF calls small
165
+ seen = set()
166
+
167
+ def dedup_cap(items: list[str], cap: int) -> list[str]:
168
+ out = []
169
+ for s in items:
170
+ k = _norm(s)
171
+ if not k or k in seen:
172
+ continue
173
+ out.append(s)
174
+ seen.add(k)
175
+ if len(out) >= cap:
176
+ break
177
+ return out
178
+
179
+ hints = dedup_cap(hints_prioritized, cap=3) + dedup_cap(hints_general, cap=2)
180
+ return hints, iata_cand, icao_cand
181
+
182
+
183
+ def _parse_coord(s: Optional[str]) -> float:
184
+ """Return float value or NaN for None/blank/invalid strings."""
185
+ if s is None:
186
+ return float("nan")
187
+ s = s.strip()
188
+ if s == "":
189
+ return float("nan")
190
+ try:
191
+ return float(s)
192
+ except ValueError:
193
+ return float("nan")
194
+
195
+
196
+ @lru_cache(maxsize=1)
197
+ def _load_data() -> dict[str, Any]:
198
+ """
199
+ Loads and caches airport rows and vectorized fields.
200
+ Expects CSV columns: icao,iata,name,city,subd,country,elevation,lat,lon,tz,lid
201
+ """
202
+ rows: list[dict[str, Any]] = []
203
+ lat_rad: list[float] = []
204
+ lon_rad: list[float] = []
205
+ keys: list[str] = [] # normalized text used for fuzzy matching
206
+ codes: list[tuple[str, str]] = [] # (iata, icao)
207
+ has_coords: list[bool] = []
208
+
209
+ with open(CSV_PATH, newline="", encoding="utf-8") as f:
210
+ reader = csv.DictReader(f)
211
+ for r in reader:
212
+ name = (r.get("name") or "").strip()
213
+ city = (r.get("city") or "").strip()
214
+ iata = (r.get("iata") or "").strip() or None
215
+ icao = (r.get("icao") or "").strip() or None
216
+
217
+ # Robust coords: keep NaN if missing/invalid
218
+ lat_s = cast(Optional[str], r.get("lat"))
219
+ lon_s = cast(Optional[str], r.get("lon"))
220
+ lat = _parse_coord(lat_s)
221
+ lon = _parse_coord(lon_s)
222
+
223
+ valid = not (math.isnan(lat) or math.isnan(lon))
224
+
225
+ rows.append(
226
+ {
227
+ "name": name,
228
+ "city": city,
229
+ "iata": iata,
230
+ "icao": icao,
231
+ "lat": lat,
232
+ "lon": lon,
233
+ }
234
+ )
235
+ lat_rad.append(math.radians(lat) if valid else float("nan"))
236
+ lon_rad.append(math.radians(lon) if valid else float("nan"))
237
+ has_coords.append(valid)
238
+
239
+ code_bits = (
240
+ " ".join([c for c in (iata, icao) if c]) if (iata or icao) else ""
241
+ )
242
+ keys.append(_norm(f"{name} {city} {code_bits}"))
243
+ codes.append((iata or "", icao or ""))
244
+
245
+ return {
246
+ "rows": rows,
247
+ "lat_rad": np.array(lat_rad, dtype=float),
248
+ "lon_rad": np.array(lon_rad, dtype=float),
249
+ "keys": np.array(keys, dtype=object),
250
+ "codes": codes,
251
+ "has_coords": np.array(has_coords, dtype=bool),
252
+ }
253
+
254
+
255
+ # ---------- Main ----------
256
+ def resolve_airport(
257
+ query: str,
258
+ places_response: list[GooglePlace],
259
+ max_distance_km: Optional[float] = 200,
260
+ max_results: Optional[int] = 5,
261
+ confidence_threshold: Optional[float] = 0.5,
262
+ text_weight: float = 0.5,
263
+ ) -> list[ResolvedAirport]:
264
+ """
265
+ Resolve airport candidates given a query and a list of Places responses.
266
+ Args:
267
+ query: The text query to resolve an airport from.
268
+ places_response: The list of Places responses to resolve an airport from.
269
+ max_distance_km: The maximum distance in kilometers to any of the places to consider for proximity.
270
+ max_results: The maximum number of results to return.
271
+ confidence_threshold: The confidence threshold to consider for the results. Default is 0.5.
272
+ text_weight: The weight for the text confidence.
273
+ Returns:
274
+ The list of resolved airports ordered by confidence.
275
+ """
276
+
277
+ data = _load_data()
278
+ rows: list[dict[str, Any]] = data["rows"]
279
+ n = len(rows)
280
+ if n == 0:
281
+ return []
282
+
283
+ # ---- Proximity anchors from Places ----
284
+ anchors = _place_points(places_response)
285
+ min_dist = np.full(n, np.inf, dtype=float)
286
+ if anchors:
287
+ for lat, lon in anchors:
288
+ lat1 = math.radians(lat)
289
+ lon1 = math.radians(lon)
290
+ d = _haversine_km_scalar_to_many(
291
+ lat1, lon1, data["lat_rad"], data["lon_rad"]
292
+ )
293
+ np.nan_to_num(d, copy=False, nan=np.inf) # NaN coords -> ∞
294
+ np.minimum(min_dist, d, out=min_dist)
295
+
296
+ prox = np.zeros(n, dtype=float)
297
+ if anchors:
298
+ prox = 100.0 * np.exp(-min_dist / float(DIST_KM_SCALE))
299
+
300
+ # ---- Text score: best across augmented queries ----
301
+ hints, iata_from_places, icao_from_places = _place_hints_and_codes(places_response)
302
+ q_variants = [_norm(query)] + [_norm(f"{query} {h}") for h in hints]
303
+ # Single cdist call over up to 1+5 variants keeps things fast
304
+ scores_matrix = process.cdist(q_variants, data["keys"], scorer=fuzz.token_set_ratio)
305
+ text_scores = np.array(scores_matrix.max(axis=0), dtype=float)
306
+
307
+ # ---- Code bonuses ----
308
+ # 1) If the *user* typed a code, stronger bonus
309
+ iata_q, icao_q = _looks_like_code(query)
310
+ if iata_q or icao_q:
311
+ if iata_q:
312
+ text_scores += (
313
+ np.fromiter(
314
+ ((1.0 if iata_q == iata else 0.0) for iata, _ in data["codes"]),
315
+ float,
316
+ count=n,
317
+ )
318
+ * CODE_BONUS_QUERY
319
+ )
320
+ if icao_q:
321
+ text_scores += (
322
+ np.fromiter(
323
+ ((1.0 if icao_q == icao else 0.0) for _, icao in data["codes"]),
324
+ float,
325
+ count=n,
326
+ )
327
+ * CODE_BONUS_QUERY
328
+ )
329
+
330
+ # 2) If Places hints include codes (e.g., “JFK Terminal 4”), soft bonus
331
+ if iata_from_places:
332
+ text_scores += (
333
+ np.fromiter(
334
+ (
335
+ (1.0 if (iata in iata_from_places) else 0.0)
336
+ for iata, _ in data["codes"]
337
+ ),
338
+ float,
339
+ count=n,
340
+ )
341
+ * CODE_BONUS_PLACES
342
+ )
343
+ if icao_from_places:
344
+ text_scores += (
345
+ np.fromiter(
346
+ (
347
+ (1.0 if (icao in icao_from_places) else 0.0)
348
+ for _, icao in data["codes"]
349
+ ),
350
+ float,
351
+ count=n,
352
+ )
353
+ * CODE_BONUS_PLACES
354
+ )
355
+
356
+ # Cap to 0..100
357
+ text_scores = np.clip(text_scores, 0.0, 100.0)
358
+
359
+ # ---- Blend + optional radius mask ----
360
+ final = text_weight * text_scores + (1.0 - text_weight) * prox
361
+
362
+ # Apply mask only if we have anchors and caller asked for one.
363
+ # IMPORTANT: rows without coords are still included (mask keeps them).
364
+ if anchors and max_distance_km is not None:
365
+ mask = (~data["has_coords"]) | (min_dist <= float(max_distance_km))
366
+ else:
367
+ mask = np.ones(n, dtype=bool)
368
+
369
+ final_masked = np.where(mask, final, -np.inf)
370
+ order = np.argsort(-final_masked)
371
+ top = order[: max_results or DEFAULT_MAX_RESULTS]
372
+
373
+ results: list[ResolvedAirport] = []
374
+ for idx in top:
375
+ if final_masked[idx] == -np.inf:
376
+ break
377
+ r = rows[idx]
378
+ text_confidence = float(text_scores[idx] / 100.0)
379
+ proximity_confidence = float(prox[idx] / 100.0)
380
+ if (
381
+ confidence_threshold is not None
382
+ and (text_confidence + proximity_confidence) / 2.0 < confidence_threshold
383
+ ):
384
+ continue
385
+ results.append(
386
+ ResolvedAirport(
387
+ name=r["name"],
388
+ city=r["city"],
389
+ iata_code=r["iata"] or None,
390
+ icao_code=r["icao"] or None,
391
+ confidence=(text_confidence + proximity_confidence) / 2.0,
392
+ )
393
+ )
394
+
395
+ results.sort(key=lambda x: x.confidence, reverse=True)
396
+
397
+ return results
@@ -0,0 +1,98 @@
1
+ """
2
+ Transport abstractions for Google Places clients.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Optional, Protocol
8
+
9
+ from google.api_core.client_options import ClientOptions
10
+ from google.maps.places_v1 import PlacesAsyncClient, PlacesClient
11
+
12
+
13
+ class SyncPlacesTransport(Protocol):
14
+ """Protocol for synchronous Places API transport."""
15
+
16
+ def autocomplete_places(self, *, request: dict[str, Any], **kwargs: Any) -> Any: ...
17
+
18
+ def search_text(
19
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
20
+ ) -> Any: ...
21
+
22
+ def get_place(
23
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
24
+ ) -> Any: ...
25
+
26
+ def close(self) -> None: ...
27
+
28
+
29
+ class AsyncPlacesTransport(Protocol):
30
+ """Protocol for asynchronous Places API transport."""
31
+
32
+ async def autocomplete_places(
33
+ self, *, request: dict[str, Any], **kwargs: Any
34
+ ) -> Any: ...
35
+
36
+ async def search_text(
37
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
38
+ ) -> Any: ...
39
+
40
+ async def get_place(
41
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
42
+ ) -> Any: ...
43
+
44
+ async def close(self) -> None: ...
45
+
46
+
47
+ class GoogleSyncTransport:
48
+ """Synchronous transport implementation for Google Places API."""
49
+
50
+ def __init__(self, api_key: str, client: Optional[PlacesClient] = None) -> None:
51
+ self.client = client or PlacesClient(
52
+ client_options=ClientOptions(api_key=api_key)
53
+ )
54
+
55
+ def autocomplete_places(self, *, request: dict[str, Any], **kwargs: Any) -> Any:
56
+ return self.client.autocomplete_places(request=request, **kwargs)
57
+
58
+ def search_text(
59
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
60
+ ) -> Any:
61
+ return self.client.search_text(request=request, metadata=metadata)
62
+
63
+ def get_place(
64
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
65
+ ) -> Any:
66
+ return self.client.get_place(request=request, metadata=metadata)
67
+
68
+ def close(self) -> None:
69
+ self.client.transport.close()
70
+
71
+
72
+ class GoogleAsyncTransport:
73
+ """Asynchronous transport implementation for Google Places API."""
74
+
75
+ def __init__(
76
+ self, api_key: str, client: Optional[PlacesAsyncClient] = None
77
+ ) -> None:
78
+ self.client = client or PlacesAsyncClient(
79
+ client_options=ClientOptions(api_key=api_key)
80
+ )
81
+
82
+ async def autocomplete_places(
83
+ self, *, request: dict[str, Any], **kwargs: Any
84
+ ) -> Any:
85
+ return await self.client.autocomplete_places(request=request, **kwargs)
86
+
87
+ async def search_text(
88
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
89
+ ) -> Any:
90
+ return await self.client.search_text(request=request, metadata=metadata)
91
+
92
+ async def get_place(
93
+ self, *, request: dict[str, Any], metadata: tuple[tuple[str, str], ...]
94
+ ) -> Any:
95
+ return await self.client.get_place(request=request, metadata=metadata)
96
+
97
+ async def close(self) -> None:
98
+ await self.client.transport.close()
@@ -1,6 +1,15 @@
1
1
  """
2
- Logging configuration for bookalimo package.
3
- Public SDK-style logging with built-in redaction helpers.
2
+ Logging utilities for the Bookalimo SDK.
3
+
4
+ The SDK uses Python's standard logging module. To enable debug logging,
5
+ configure the 'bookalimo' logger or set the BOOKALIMO_LOG_LEVEL environment variable.
6
+
7
+ Example:
8
+ import logging
9
+ logging.getLogger('bookalimo').setLevel(logging.DEBUG)
10
+
11
+ # Or via environment variable
12
+ export BOOKALIMO_LOG_LEVEL=DEBUG
4
13
  """
5
14
 
6
15
  from __future__ import annotations
@@ -18,9 +27,41 @@ from typing_extensions import ParamSpec
18
27
  P = ParamSpec("P")
19
28
  R = TypeVar("R")
20
29
 
30
+
31
+ def _level_from_env() -> int | None:
32
+ """Get log level from BOOKALIMO_LOG_LEVEL environment variable."""
33
+ lvl = os.getenv("BOOKALIMO_LOG_LEVEL")
34
+ if not lvl:
35
+ return None
36
+ try:
37
+ return int(lvl)
38
+ except ValueError:
39
+ try:
40
+ return logging._nameToLevel.get(lvl.upper(), None)
41
+ except Exception:
42
+ return None
43
+
44
+
21
45
  logger = logging.getLogger("bookalimo")
22
- logger.addHandler(logging.NullHandler())
23
- logger.setLevel(logging.WARNING)
46
+
47
+ # Apply environment variable level if set, otherwise use WARNING as default
48
+ env_level = _level_from_env()
49
+ logger.setLevel(env_level if env_level is not None else logging.WARNING)
50
+
51
+ # If user set BOOKALIMO_LOG_LEVEL, they expect to see logs - add console handler
52
+ if env_level is not None:
53
+ # Only add console handler if one doesn't already exist
54
+ if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
55
+ console_handler = logging.StreamHandler()
56
+ console_handler.setLevel(env_level)
57
+ formatter = logging.Formatter(
58
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
59
+ )
60
+ console_handler.setFormatter(formatter)
61
+ logger.addHandler(console_handler)
62
+ else:
63
+ # If no env var is set, use NullHandler (library default behavior)
64
+ logger.addHandler(logging.NullHandler())
24
65
 
25
66
  REDACTED = "******"
26
67
 
@@ -138,44 +179,6 @@ def get_logger(name: str | None = None) -> logging.Logger:
138
179
  return logger
139
180
 
140
181
 
141
- def enable_debug_logging(level: int | None = None) -> None:
142
- level = level or _level_from_env() or logging.DEBUG
143
- logger.setLevel(level)
144
-
145
- has_real_handler = any(
146
- not isinstance(h, logging.NullHandler) for h in logger.handlers
147
- )
148
- if not has_real_handler:
149
- handler = logging.StreamHandler()
150
- formatter = logging.Formatter(
151
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
152
- )
153
- handler.setFormatter(formatter)
154
- logger.addHandler(handler)
155
-
156
- logger.info("bookalimo logging enabled at %s", logging.getLevelName(logger.level))
157
-
158
-
159
- def disable_debug_logging() -> None:
160
- logger.setLevel(logging.WARNING)
161
- for handler in logger.handlers[:]:
162
- if not isinstance(handler, logging.NullHandler):
163
- logger.removeHandler(handler)
164
-
165
-
166
- def _level_from_env() -> int | None:
167
- lvl = os.getenv("BOOKALIMO_LOG_LEVEL")
168
- if not lvl:
169
- return None
170
- try:
171
- return int(lvl)
172
- except ValueError:
173
- try:
174
- return logging._nameToLevel.get(lvl.upper(), None)
175
- except Exception:
176
- return None
177
-
178
-
179
182
  # ---- decorator for async methods --------------------------------------------
180
183
 
181
184
 
@@ -0,0 +1,103 @@
1
+ """Pydantic schemas for the Bookalimo SDK."""
2
+
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
+ from .places import (
35
+ AutocompletePlacesRequest,
36
+ AutocompletePlacesResponse,
37
+ Circle,
38
+ EVConnectorType,
39
+ FormattableText,
40
+ GeocodingRequest,
41
+ GetPlaceRequest,
42
+ LocationBias,
43
+ LocationRestriction,
44
+ Place,
45
+ PlacePrediction,
46
+ PlaceType,
47
+ QueryPrediction,
48
+ RankPreference,
49
+ SearchTextRequest,
50
+ StringRange,
51
+ StructuredFormat,
52
+ Suggestion,
53
+ )
54
+
55
+ __all__ = [
56
+ "RateType",
57
+ "LocationType",
58
+ "MeetGreetType",
59
+ "RewardType",
60
+ "ReservationStatus",
61
+ "CardHolderType",
62
+ "City",
63
+ "Address",
64
+ "Airport",
65
+ "Location",
66
+ "Stop",
67
+ "Passenger",
68
+ "Reward",
69
+ "CreditCard",
70
+ "MeetGreetAdditional",
71
+ "Price",
72
+ "Reservation",
73
+ "EditableReservationRequest",
74
+ "PriceRequest",
75
+ "PriceResponse",
76
+ "DetailsRequest",
77
+ "DetailsResponse",
78
+ "BookRequest",
79
+ "BookResponse",
80
+ "ListReservationsRequest",
81
+ "ListReservationsResponse",
82
+ "GetReservationRequest",
83
+ "EditReservationResponse",
84
+ "PlaceType",
85
+ "RankPreference",
86
+ "EVConnectorType",
87
+ "StringRange",
88
+ "FormattableText",
89
+ "StructuredFormat",
90
+ "Circle",
91
+ "LocationBias",
92
+ "LocationRestriction",
93
+ "Place",
94
+ "AutocompletePlacesResponse",
95
+ "PlacePrediction",
96
+ "QueryPrediction",
97
+ "Suggestion",
98
+ "GetPlaceRequest",
99
+ "AutocompletePlacesRequest",
100
+ "GeocodingRequest",
101
+ "SearchTextRequest",
102
+ "Credentials",
103
+ ]