aspyx-service 0.10.3__py3-none-any.whl → 0.10.4__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.

Potentially problematic release.


This version of aspyx-service might be problematic. Click here for more details.

aspyx_service/__init__.py CHANGED
@@ -4,12 +4,14 @@ This module provides the core Aspyx service management framework allowing for se
4
4
 
5
5
  from aspyx.di import module
6
6
 
7
- from .service import ServiceException, Server, Channel, ComponentDescriptor, inject_service, ChannelAddress, ChannelInstances, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
8
- from .channels import HTTPXChannel, DispatchJSONChannel
7
+ from .service import AuthorizationException, MissingTokenException, RemoteServiceException, ServiceCommunicationException, TokenException, TokenExpiredException, InvalidTokenException, component_services, ServiceException, Server, Channel, ComponentDescriptor, inject_service, ChannelAddress, ChannelInstances, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
8
+ from .channels import HTTPXChannel, DispatchJSONChannel, TokenContext
9
9
  from .registries import ConsulComponentRegistry
10
- from .server import FastAPIServer
10
+ from .server import FastAPIServer, RequestContext
11
11
  from .healthcheck import health_checks, health_check, HealthCheckManager, HealthStatus
12
12
  from .restchannel import RestChannel, post, get, put, delete, QueryParam, Body, rest
13
+ from .session import Session, SessionManager
14
+ from .authorization import AuthorizationManager, AbstractAuthorizationFactory, AuthorizationException
13
15
 
14
16
 
15
17
  @module()
@@ -38,6 +40,25 @@ __all__ = [
38
40
  "service",
39
41
  "implementation",
40
42
  "inject_service",
43
+ "component_services",
44
+ "RemoteServiceException",
45
+ "ServiceCommunicationException",
46
+ "TokenException",
47
+ "TokenExpiredException",
48
+ "InvalidTokenException",
49
+ "MissingTokenException",
50
+ "AuthorizationException",
51
+
52
+ # authorization
53
+
54
+ "AuthorizationManager",
55
+ "AbstractAuthorizationFactory",
56
+ "AuthorizationException",
57
+
58
+ # session
59
+
60
+ "Session",
61
+ "SessionManager",
41
62
 
42
63
  # healthcheck
43
64
 
@@ -54,6 +75,7 @@ __all__ = [
54
75
 
55
76
  "HTTPXChannel",
56
77
  "DispatchJSONChannel",
78
+ "TokenContext",
57
79
 
58
80
  # rest
59
81
 
@@ -69,8 +91,10 @@ __all__ = [
69
91
  # registries
70
92
 
71
93
  "ConsulComponentRegistry",
94
+ "RequestContext",
72
95
 
73
96
  # server
74
97
 
75
- "FastAPIServer"
98
+ "FastAPIServer",
99
+ "RequestContext"
76
100
  ]
@@ -0,0 +1,135 @@
1
+ """
2
+ authorization logic
3
+ """
4
+ import inspect
5
+ from abc import abstractmethod, ABC
6
+ from typing import Optional, Callable
7
+
8
+ from aspyx.di import injectable, inject, order
9
+ from aspyx.di.aop import Invocation
10
+ from aspyx.reflection import TypeDescriptor, Decorators
11
+
12
+
13
+ class AuthorizationException(Exception):
14
+ """
15
+ Any authorization exception
16
+ """
17
+ pass
18
+
19
+ def get_method_class(method):
20
+ if inspect.ismethod(method) or inspect.isfunction(method):
21
+ qualname = method.__qualname__
22
+ parts = qualname.split('.')
23
+ if len(parts) > 1:
24
+ cls_name = parts[-2]
25
+ module = inspect.getmodule(method)
26
+ if module:
27
+ for name, obj in inspect.getmembers(module, inspect.isclass):
28
+ if name == cls_name and hasattr(obj, method.__name__):
29
+ return obj
30
+ return None
31
+
32
+ @injectable()
33
+ class AuthorizationManager:
34
+ """
35
+ The authorization manager is used to remember and execute pluggable authorization checks.
36
+ """
37
+ class Authorization():
38
+ """
39
+ Base class for authorization checks
40
+ """
41
+ def check(self, invocation: Invocation):
42
+ """
43
+ execute the authorization check. Throws an exception in case of violations,
44
+ """
45
+ pass
46
+
47
+ class AuthorizationFactory(ABC):
48
+ """
49
+ An authorization factory is used to create possible authorization checks given a method descriptor
50
+ """
51
+
52
+ def __init__(self, order = 0):
53
+ self.order = order
54
+
55
+ @abstractmethod
56
+ def compute_authorization(self, method_descriptor: TypeDescriptor.MethodDescriptor) -> Optional['AuthorizationManager.Authorization']:
57
+ """
58
+ return a possible authorization check given a method descriptor
59
+ Args:
60
+ method_descriptor: the corresponding method descriptor
61
+
62
+ Returns:
63
+ an authorization check or None
64
+ """
65
+ pass
66
+
67
+ # constructor
68
+
69
+ def __init__(self):
70
+ self.factories : list[AuthorizationManager.AuthorizationFactory] = []
71
+ self.checks : dict[Callable, list[AuthorizationManager.Authorization]] = {}
72
+
73
+ # public
74
+
75
+ def register_factory(self, factory: 'AuthorizationManager.AuthorizationFactory'):
76
+ self.factories.append(factory)
77
+
78
+ self.factories.sort(key=lambda factory: factory.order)
79
+
80
+ # internal
81
+
82
+ def compute_checks(self, func: Callable) -> list[Authorization]:
83
+ checks = []
84
+
85
+ clazz = get_method_class(func)
86
+
87
+ descriptor = TypeDescriptor.for_type(clazz).get_method(func.__name__)
88
+
89
+ for factory in self.factories:
90
+ check = factory.compute_authorization(descriptor)
91
+ if check is not None:
92
+ checks.append(check)
93
+
94
+ return checks
95
+
96
+ def get_checks(self, func: Callable) -> list[Authorization]:
97
+ """
98
+ return a list of authorization checks given a function.
99
+
100
+ Args:
101
+ func: the corresponding function.
102
+
103
+ Returns:
104
+ list of authorization checks
105
+ """
106
+ checks = self.checks.get(func, None)
107
+ if checks is None:
108
+ checks = self.compute_checks(func)
109
+ self.checks[func] = checks
110
+ print(checks)
111
+
112
+ return checks
113
+
114
+ def check(self, invocation: Invocation) -> Optional[Authorization]:
115
+ for check in self.get_checks(invocation.func):
116
+ check.check(invocation)
117
+
118
+ class AbstractAuthorizationFactory(AuthorizationManager.AuthorizationFactory):
119
+ """
120
+ Abstract base class for authorization factories
121
+ """
122
+
123
+ # constructor
124
+
125
+ def __init__(self):
126
+ super().__init__(0)
127
+
128
+ if Decorators.has_decorator(type(self), order):
129
+ self.order = Decorators.get_decorator(type(self), order).args[0]
130
+
131
+ # inject
132
+
133
+ @inject()
134
+ def set_authorization_manager(self, authorization_manager: AuthorizationManager):
135
+ authorization_manager.register_factory(self)
aspyx_service/channels.py CHANGED
@@ -3,22 +3,62 @@ Service management and dependency injection framework.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- import json
7
- from dataclasses import is_dataclass, asdict, fields
6
+ import typing
7
+ from contextlib import contextmanager
8
+ from dataclasses import is_dataclass, fields
8
9
  from typing import Type, Optional, Any, Callable
9
10
 
11
+ import httpx
10
12
  import msgpack
11
- from httpx import Client, AsyncClient
13
+ from httpx import Client, AsyncClient, USE_CLIENT_DEFAULT
12
14
  from pydantic import BaseModel
13
15
 
14
16
  from aspyx.di.configuration import inject_value
15
17
  from aspyx.reflection import DynamicProxy, TypeDescriptor
16
- from aspyx.threading import ThreadLocal
17
- from .service import ServiceManager, ServiceCommunicationException
18
+ from aspyx.threading import ThreadLocal, ContextLocal
19
+ from .service import ServiceManager, ServiceCommunicationException, TokenExpiredException, InvalidTokenException, \
20
+ AuthorizationException, MissingTokenException
18
21
 
19
22
  from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
20
- from .serialization import get_deserializer
23
+ from .serialization import get_deserializer, TypeDeserializer, TypeSerializer, get_serializer
21
24
 
25
+ class TokenContext:
26
+ """
27
+ TokeContext covers two context locals for both the access and - optional - refresh topen
28
+ """
29
+ access_token = ContextLocal[str]("access_token", default=None)
30
+ refresh_token = ContextLocal[str]("refresh_token", default=None)
31
+
32
+ @classmethod
33
+ def get_access_token(cls) -> Optional[str]:
34
+ return cls.access_token.get()
35
+
36
+ @classmethod
37
+ def get_refresh_token(cls) -> Optional[str]:
38
+ return cls.refresh_token.get()
39
+
40
+
41
+ @classmethod
42
+ def set(cls, access_token: str, refresh_token: Optional[str] = None):
43
+ cls.access_token.set(access_token)
44
+ if refresh_token:
45
+ cls.refresh_token.set(refresh_token)
46
+
47
+ @classmethod
48
+ def clear(cls):
49
+ cls.access_token.set(None)
50
+ cls.refresh_token.set(None)
51
+
52
+ @classmethod
53
+ @contextmanager
54
+ def use(cls, access_token: str, refresh_token: Optional[str] = None):
55
+ access_token = cls.access_token.set(access_token)
56
+ refresh_token = cls.refresh_token.set(refresh_token)
57
+ try:
58
+ yield
59
+ finally:
60
+ cls.access_token.reset(access_token)
61
+ cls.refresh_token.reset(refresh_token)
22
62
 
23
63
  class HTTPXChannel(Channel):
24
64
  __slots__ = [
@@ -26,7 +66,8 @@ class HTTPXChannel(Channel):
26
66
  "async_client",
27
67
  "service_names",
28
68
  "deserializers",
29
- "timeout"
69
+ "timeout",
70
+ "optimize_serialization"
30
71
  ]
31
72
 
32
73
  # class properties
@@ -39,8 +80,7 @@ class HTTPXChannel(Channel):
39
80
  @classmethod
40
81
  def to_dict(cls, obj: Any) -> Any:
41
82
  if isinstance(obj, BaseModel):
42
- return obj.dict()
43
-
83
+ return obj.model_dump()
44
84
 
45
85
  elif is_dataclass(obj):
46
86
  return {
@@ -57,10 +97,6 @@ class HTTPXChannel(Channel):
57
97
 
58
98
  return obj
59
99
 
60
- @classmethod
61
- def to_json(cls, obj) -> str:
62
- return json.dumps(cls.to_dict(obj))
63
-
64
100
  # constructor
65
101
 
66
102
  def __init__(self):
@@ -68,7 +104,9 @@ class HTTPXChannel(Channel):
68
104
 
69
105
  self.timeout = 1000.0
70
106
  self.service_names: dict[Type, str] = {}
107
+ self.serializers: dict[Callable, list[Callable]] = {}
71
108
  self.deserializers: dict[Callable, Callable] = {}
109
+ self.optimize_serialization = True
72
110
 
73
111
  # inject
74
112
 
@@ -78,7 +116,27 @@ class HTTPXChannel(Channel):
78
116
 
79
117
  # protected
80
118
 
81
- def get_deserializer(self, type: Type, method: Callable) -> Type:
119
+ def serialize_args(self, invocation: DynamicProxy.Invocation) -> list[Any]:
120
+ deserializers = self.get_serializers(invocation.type, invocation.method)
121
+
122
+ args = list(invocation.args)
123
+ for index, deserializer in enumerate(deserializers):
124
+ args[index] = deserializer(args[index])
125
+
126
+ return args
127
+
128
+ def get_serializers(self, type: Type, method: Callable) -> list[TypeSerializer]:
129
+ serializers = self.serializers.get(method, None)
130
+ if serializers is None:
131
+ param_types = TypeDescriptor.for_type(type).get_method(method.__name__).param_types
132
+
133
+ serializers = [get_serializer(type) for type in param_types]
134
+
135
+ self.serializers[method] = serializers
136
+
137
+ return serializers
138
+
139
+ def get_deserializer(self, type: Type, method: Callable) -> TypeDeserializer:
82
140
  deserializer = self.deserializers.get(method, None)
83
141
  if deserializer is None:
84
142
  return_type = TypeDescriptor.for_type(type).get_method(method.__name__).return_type
@@ -125,6 +183,79 @@ class HTTPXChannel(Channel):
125
183
  def make_async_client(self) -> AsyncClient:
126
184
  return AsyncClient() # base_url=url
127
185
 
186
+ def request(self, http_method: str, url: str, json: Optional[typing.Any] = None,
187
+ params: Optional[Any] = None, headers: Optional[Any] = None,
188
+ timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
189
+
190
+ token = TokenContext.get_access_token()
191
+ if token is not None:
192
+ if headers is None: # None is also valid!
193
+ headers = {}
194
+
195
+ ## add bearer token
196
+
197
+ headers["Authorization"] = f"Bearer {token}"
198
+
199
+ try:
200
+ response = self.get_client().request(http_method, url, params=params, json=json, headers=headers, timeout=timeout, content=content)
201
+ response.raise_for_status()
202
+ except httpx.RequestError as e:
203
+ raise ServiceCommunicationException(str(e)) from e
204
+
205
+ except httpx.HTTPStatusError as e:
206
+ if e.response.status_code == 401:
207
+ www_auth = e.response.headers.get("www-authenticate", "")
208
+ if "invalid_token" in www_auth:
209
+ if 'expired' in www_auth:
210
+ raise TokenExpiredException() from e
211
+ elif 'missing' in www_auth:
212
+ raise MissingTokenException() from e
213
+ else:
214
+ raise InvalidTokenException() from e
215
+
216
+ raise AuthorizationException(str(e)) from e
217
+ except httpx.HTTPError as e:
218
+ raise RemoteServiceException(str(e)) from e
219
+
220
+ return response
221
+
222
+ async def request_async(self, http_method: str, url: str, json: Optional[typing.Any] = None,
223
+ params: Optional[Any] = None, headers: Optional[Any] = None,
224
+ timeout: Any = USE_CLIENT_DEFAULT, content: Optional[Any] = None) -> httpx.Response:
225
+
226
+ token = TokenContext.get_access_token()
227
+ if token is not None:
228
+ if headers is None: # None is also valid!
229
+ headers = {}
230
+
231
+ ## add bearer token
232
+
233
+ headers["Authorization"] = f"Bearer {token}"
234
+
235
+ try:
236
+ response = await self.get_async_client().request(http_method, url, params=params, json=json, headers=headers,
237
+ timeout=timeout, content=content)
238
+ response.raise_for_status()
239
+ except httpx.RequestError as e:
240
+ raise ServiceCommunicationException(str(e)) from e
241
+
242
+ except httpx.HTTPStatusError as e:
243
+ if e.response.status_code == 401:
244
+ www_auth = e.response.headers.get("www-authenticate", "")
245
+ if "invalid_token" in www_auth:
246
+ if 'expired' in www_auth:
247
+ raise TokenExpiredException() from e
248
+ elif 'missing' in www_auth:
249
+ raise MissingTokenException() from e
250
+ else:
251
+ raise InvalidTokenException() from e
252
+
253
+ raise RemoteServiceException(str(e)) from e
254
+ except httpx.HTTPError as e:
255
+ raise RemoteServiceException(str(e)) from e
256
+
257
+ return response
258
+
128
259
  class Request(BaseModel):
129
260
  method: str # component:service:method
130
261
  args: tuple[Any, ...]
@@ -158,20 +289,25 @@ class DispatchJSONChannel(HTTPXChannel):
158
289
 
159
290
  def invoke(self, invocation: DynamicProxy.Invocation):
160
291
  service_name = self.service_names[invocation.type] # map type to registered service name
161
- request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
162
- args=invocation.args)
163
292
 
164
- dict = self.to_dict(request)
165
- try:
166
- result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout).json())
167
- if result.exception is not None:
168
- raise RemoteServiceException(f"server side exception {result.exception}")
293
+ request : dict = {
294
+ "method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
295
+ #"args": invocation.args
296
+ }
169
297
 
170
- return self.get_deserializer(invocation.type, invocation.method)(result.result)
171
- except ServiceCommunicationException:
172
- raise
298
+ if self.optimize_serialization:
299
+ request["args"] = self.serialize_args(invocation)
300
+ else:
301
+ request["args"] = self.to_dict(invocation.args)
173
302
 
174
- except RemoteServiceException:
303
+ try:
304
+ http_result = self.request( "post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
305
+ result = http_result.json()
306
+ if result["exception"] is not None:
307
+ raise RemoteServiceException(f"server side exception {result['exception']}")
308
+
309
+ return self.get_deserializer(invocation.type, invocation.method)(result["result"])
310
+ except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
175
311
  raise
176
312
 
177
313
  except Exception as e:
@@ -180,22 +316,25 @@ class DispatchJSONChannel(HTTPXChannel):
180
316
 
181
317
  async def invoke_async(self, invocation: DynamicProxy.Invocation):
182
318
  service_name = self.service_names[invocation.type] # map type to registered service name
183
- request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
184
- args=invocation.args)
185
- dict = self.to_dict(request)
186
- try:
187
- data = await self.get_async_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout)
188
- result = Response(**data.json())
319
+ request : dict = {
320
+ "method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
321
+ }
189
322
 
190
- if result.exception is not None:
191
- raise RemoteServiceException(f"server side exception {result.exception}")
323
+ if self.optimize_serialization:
324
+ request["args"] = self.serialize_args(invocation)
325
+ else:
326
+ request["args"] = self.to_dict(invocation.args)
192
327
 
193
- return self.get_deserializer(invocation.type, invocation.method)(result.result)
328
+ try:
329
+ data = await self.request_async("post", f"{self.get_url()}/invoke", json=request, timeout=self.timeout)
330
+ result = data.json()
194
331
 
195
- except ServiceCommunicationException:
196
- raise
332
+ if result["exception"] is not None:
333
+ raise RemoteServiceException(f"server side exception {result['exception']}")
197
334
 
198
- except RemoteServiceException:
335
+ return self.get_deserializer(invocation.type, invocation.method)(result["result"])
336
+
337
+ except (ServiceCommunicationException, AuthorizationException, RemoteServiceException) as e:
199
338
  raise
200
339
 
201
340
  except Exception as e:
@@ -221,13 +360,19 @@ class DispatchMSPackChannel(HTTPXChannel):
221
360
 
222
361
  def invoke(self, invocation: DynamicProxy.Invocation):
223
362
  service_name = self.service_names[invocation.type] # map type to registered service name
224
- request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
225
- args=invocation.args)
363
+ request: dict = {
364
+ "method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
365
+ }
366
+
367
+ if self.optimize_serialization:
368
+ request["args"] = self.serialize_args(invocation)
369
+ else:
370
+ request["args"] = self.to_dict(invocation.args)
226
371
 
227
372
  try:
228
- packed = msgpack.packb(self.to_dict(request), use_bin_type=True)
373
+ packed = msgpack.packb(request, use_bin_type=True)
229
374
 
230
- response = self.get_client().post(
375
+ response = self.request("post",
231
376
  f"{self.get_url()}/invoke",
232
377
  content=packed,
233
378
  headers={"Content-Type": "application/msgpack"},
@@ -241,6 +386,25 @@ class DispatchMSPackChannel(HTTPXChannel):
241
386
 
242
387
  return self.get_deserializer(invocation.type, invocation.method)(result["result"])
243
388
 
389
+
390
+ except httpx.RequestError as e:
391
+ raise ServiceCommunicationException(str(e)) from e
392
+
393
+ except httpx.HTTPStatusError as e:
394
+ if e.response.status_code == 401:
395
+ www_auth = e.response.headers.get("www-authenticate", "")
396
+ if "invalid_token" in www_auth:
397
+ if 'expired' in www_auth:
398
+ raise TokenExpiredException() from e
399
+ elif 'missing' in www_auth:
400
+ raise MissingTokenException() from e
401
+ else:
402
+ raise InvalidTokenException() from e
403
+
404
+ raise RemoteServiceException(str(e)) from e
405
+ except httpx.HTTPError as e:
406
+ raise RemoteServiceException(str(e)) from e
407
+
244
408
  except ServiceCommunicationException:
245
409
  raise
246
410
 
@@ -252,13 +416,19 @@ class DispatchMSPackChannel(HTTPXChannel):
252
416
 
253
417
  async def invoke_async(self, invocation: DynamicProxy.Invocation):
254
418
  service_name = self.service_names[invocation.type] # map type to registered service name
255
- request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
256
- args=invocation.args)
419
+ request: dict = {
420
+ "method": f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}"
421
+ }
422
+
423
+ if self.optimize_serialization:
424
+ request["args"] = self.serialize_args(invocation)
425
+ else:
426
+ request["args"] = self.to_dict(invocation.args)
257
427
 
258
428
  try:
259
- packed = msgpack.packb(self.to_dict(request), use_bin_type=True)
429
+ packed = msgpack.packb(request, use_bin_type=True)
260
430
 
261
- response = await self.get_async_client().post(
431
+ response = await self.request_async("post",
262
432
  f"{self.get_url()}/invoke",
263
433
  content=packed,
264
434
  headers={"Content-Type": "application/msgpack"},
@@ -272,6 +442,25 @@ class DispatchMSPackChannel(HTTPXChannel):
272
442
 
273
443
  return self.get_deserializer(invocation.type, invocation.method)(result["result"])
274
444
 
445
+ except httpx.RequestError as e:
446
+ raise ServiceCommunicationException(str(e)) from e
447
+
448
+ except httpx.HTTPStatusError as e:
449
+ if e.response.status_code == 401:
450
+ www_auth = e.response.headers.get("www-authenticate", "")
451
+ if "invalid_token" in www_auth:
452
+ if 'expired' in www_auth:
453
+ raise TokenExpiredException() from e
454
+ elif 'missing' in www_auth:
455
+ raise MissingTokenException() from e
456
+ else:
457
+ raise InvalidTokenException() from e
458
+
459
+ raise RemoteServiceException(str(e)) from e
460
+
461
+ except httpx.HTTPError as e:
462
+ raise RemoteServiceException(str(e)) from e
463
+
275
464
  except ServiceCommunicationException:
276
465
  raise
277
466
 
@@ -117,7 +117,7 @@ class HealthCheckManager:
117
117
  "status": str(self.status),
118
118
  }
119
119
 
120
- if len(self.details) > 0:
120
+ if self.details:
121
121
  result["details"] = self.details
122
122
 
123
123
  return result
@@ -42,19 +42,19 @@ class ConsulComponentRegistry(ComponentRegistry):
42
42
  # injections
43
43
 
44
44
  @inject_value("consul.watchdog.interval", default=5)
45
- def set_interval(self, interval):
45
+ def set_watchdog_interval(self, interval):
46
46
  self.watchdog_interval = interval
47
47
 
48
48
  @inject_value("consul.healthcheck.interval", default="10s")
49
- def set_interval(self, interval):
49
+ def set_healthcheck_interval(self, interval):
50
50
  self.healthcheck_interval = interval
51
51
 
52
52
  @inject_value("consul.healthcheck.timeout", default="3s")
53
- def set_interval(self, interval):
53
+ def set_healthcheck_timeout(self, interval):
54
54
  self.healthcheck_timeout = interval
55
55
 
56
56
  @inject_value("consul.healthcheck.deregister", default="5m")
57
- def set_interval(self, interval):
57
+ def set_healthcheck_deregister(self, interval):
58
58
  self.healthcheck_deregister = interval
59
59
 
60
60
  # lifecycle hooks
@@ -220,7 +220,7 @@ class ConsulComponentRegistry(ComponentRegistry):
220
220
 
221
221
  # only cache if non-empty
222
222
 
223
- if len(component_addresses) > 0:
223
+ if component_addresses:
224
224
  self.component_addresses[descriptor.name] = component_addresses
225
225
 
226
226
  return list(component_addresses.values())