Przeglądaj źródła

service_api: self-registering clients via @register_service

Replaces the hand-edited _CLIENT_CLASSES dict in client.py with a
registry (_registry.py) that clients populate by decorating their
class with @register_service('key'), mirroring the store's
@register_backend pattern. Adding a service client is now a new
module + one import line, no edits inside ServiceAPI.
ZiB 3 miesięcy temu
rodzic
commit
9833c7f6ea

+ 2 - 1
room_cal/service_api/_airbnb.py

@@ -16,6 +16,7 @@ from ._scripts import _assert_not_async, _npm_env, _ts_dir, _ensure_installed
 from room_cal._utils import get_config
 from ._base import ServiceClientBase, _AUTH_HINT
 from ._month_cache import MonthCache, TTLCache
+from ._registry import register_service
 from ._utils import month_bounds, months_in_range
 from .credentials import mark_stale
 from .types import CalendarEvent, DayInfo, ServiceAuthError, Reservation
@@ -119,8 +120,8 @@ def _trace_id() -> str:
   return ''.join(random.choices(string.ascii_lowercase + string.digits, k=28))
 
 
+@register_service('airbnb')
 class _AirbnbClient(ServiceClientBase):
-  service_key = 'airbnb'
   service_name = 'AirBnB'
   auth_error_cls = AirbnbAuthError
 

+ 2 - 1
room_cal/service_api/_booking.py

@@ -13,6 +13,7 @@ from ._scripts import _assert_not_async, _booking_lock, _npm_env, _ts_dir, _ensu
 from room_cal._utils import get_config
 from ._base import ServiceClientBase, _AUTH_HINT
 from ._month_cache import MonthCache
+from ._registry import register_service
 from ._utils import month_bounds, months_in_range
 from .credentials import set_credentials
 from .types import DayInfo, ServiceAuthError, Reservation
@@ -144,8 +145,8 @@ def _page_view_id() -> str:
   return ''.join(random.choices('0123456789abcdef', k=16))
 
 
+@register_service('booking')
 class _BookingClient(ServiceClientBase):
-  service_key = 'booking'
   service_name = 'Booking.com'
   auth_error_cls = BookingAuthError
 

+ 17 - 0
room_cal/service_api/_registry.py

@@ -0,0 +1,17 @@
+"""Self-registration for service clients, mirroring room_cal.store's @register_backend pattern."""
+
+_client_classes: dict[str, type] = {}
+
+
+def register_service(key: str):
+  """Class decorator: sets `service_key` and adds the class to the registry under `key`."""
+  def deco(cls):
+    cls.service_key = key
+    _client_classes[key] = cls
+    return cls
+  return deco
+
+
+def client_classes() -> dict[str, type]:
+  """Return the registry of service_key -> client class."""
+  return _client_classes

+ 6 - 11
room_cal/service_api/client.py

@@ -2,9 +2,9 @@ import asyncio
 import logging
 from datetime import date
 
-from ._airbnb import _AirbnbClient
-from ._booking import _BookingClient
+from . import _airbnb, _booking  # noqa: F401 — import registers each client via @register_service
 from ._protocols import ChatClient, ServiceClient
+from ._registry import client_classes
 from .types import DayInfo, ServiceAuthError
 
 _log = logging.getLogger(__name__)
@@ -21,11 +21,6 @@ class ServiceAPI:
   Pass a custom `clients` dict to substitute stubs for testing.
   """
 
-  _CLIENT_CLASSES: dict[str, type] = {
-    'airbnb': _AirbnbClient,
-    'booking': _BookingClient,
-  }
-
   @classmethod
   def get(cls) -> 'ServiceAPI':
     """Return the process-wide singleton, creating it on first call."""
@@ -47,7 +42,7 @@ class ServiceAPI:
       return
     self._clients = {}
     self._disabled: dict[str, str] = {}
-    for key, cls in self._CLIENT_CLASSES.items():
+    for key, cls in client_classes().items():
       try:
         self._clients[key] = cls.from_env()
       except Exception as e:
@@ -60,13 +55,13 @@ class ServiceAPI:
 
   def service_name(self, key: str) -> str:
     """Return the human-readable display name for a service key."""
-    cls = self._CLIENT_CLASSES.get(key)
+    cls = client_classes().get(key)
     return getattr(cls, 'service_name', key.capitalize()) if cls else key.capitalize()
 
 
   async def refresh_credentials(self, key: str) -> None:
     """Trigger credential refresh for the named service. Raises ServiceAuthError on failure."""
-    cls = self._CLIENT_CLASSES.get(key)
+    cls = client_classes().get(key)
     if cls is None or not hasattr(cls, 'refresh_credentials'):
       from .types import ServiceAuthError
       raise ServiceAuthError(f'No automatic refresh handler for service: {key}')
@@ -84,7 +79,7 @@ class ServiceAPI:
     if client is not None and hasattr(client, '_reload_from_env'):
       client._reload_from_env()
       return
-    cls = self._CLIENT_CLASSES.get(key)
+    cls = client_classes().get(key)
     if cls is None:
       return
     try: