aspyx-service 0.9.0__py3-none-any.whl → 0.10.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.

Potentially problematic release.


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

aspyx_service/__init__.py CHANGED
@@ -4,7 +4,7 @@ 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, ServiceAddress, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
7
+ from .service import ServiceException, Server, Channel, ComponentDescriptor, inject_service, ChannelAddress, ChannelInstances, ServiceManager, Component, Service, AbstractComponent, ComponentStatus, ComponentRegistry, implementation, health, component, service
8
8
  from .channels import HTTPXChannel, DispatchJSONChannel
9
9
  from .registries import ConsulComponentRegistry
10
10
  from .server import FastAPIServer
@@ -32,7 +32,7 @@ __all__ = [
32
32
  "ComponentDescriptor",
33
33
  "ComponentRegistry",
34
34
  "ChannelAddress",
35
- "ServiceAddress",
35
+ "ChannelInstances",
36
36
  "health",
37
37
  "component",
38
38
  "service",
@@ -41,6 +41,11 @@ __all__ = [
41
41
 
42
42
  # healthcheck
43
43
 
44
+ "health_checks",
45
+ "health_check",
46
+ "HealthStatus",
47
+ "HealthCheckManager",
48
+
44
49
  # serialization
45
50
 
46
51
  # "deserialize",
aspyx_service/channels.py CHANGED
@@ -12,7 +12,7 @@ from pydantic import BaseModel
12
12
  from aspyx.reflection import DynamicProxy, TypeDescriptor
13
13
  from .service import ServiceManager
14
14
 
15
- from .service import ComponentDescriptor, ServiceAddress, ServiceException, channel, Channel, RemoteServiceException
15
+ from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
16
16
  from .serialization import get_deserializer
17
17
 
18
18
 
@@ -26,8 +26,8 @@ class HTTPXChannel(Channel):
26
26
 
27
27
  # constructor
28
28
 
29
- def __init__(self, name):
30
- super().__init__(name)
29
+ def __init__(self):
30
+ super().__init__()
31
31
 
32
32
  self.client: Optional[Client] = None
33
33
  self.async_client: Optional[AsyncClient] = None
@@ -49,7 +49,7 @@ class HTTPXChannel(Channel):
49
49
 
50
50
  # override
51
51
 
52
- def setup(self, component_descriptor: ComponentDescriptor, address: ServiceAddress):
52
+ def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
53
53
  super().setup(component_descriptor, address)
54
54
 
55
55
  # remember service names
@@ -92,21 +92,25 @@ class Response(BaseModel):
92
92
 
93
93
  @channel("dispatch-json")
94
94
  class DispatchJSONChannel(HTTPXChannel):
95
+ """
96
+ A channel that calls a POST on th endpoint `ìnvoke` sending a request body containing information on the
97
+ called component, service and method and the arguments.
98
+ """
95
99
  # constructor
96
100
 
97
101
  def __init__(self):
98
- super().__init__("dispatch-json")
102
+ super().__init__()
99
103
 
100
104
  # internal
101
105
 
102
106
  # implement Channel
103
107
 
104
- def set_address(self, address: Optional[ServiceAddress]):
108
+ def set_address(self, address: Optional[ChannelInstances]):
105
109
  ServiceManager.logger.info("channel %s got an address %s", self.name, address)
106
110
 
107
111
  super().set_address(address)
108
112
 
109
- def setup(self, component_descriptor: ComponentDescriptor, address: ServiceAddress):
113
+ def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
110
114
  super().setup(component_descriptor, address)
111
115
 
112
116
  def invoke(self, invocation: DynamicProxy.Invocation):
@@ -146,14 +150,17 @@ class DispatchJSONChannel(HTTPXChannel):
146
150
 
147
151
  @channel("dispatch-msgpack")
148
152
  class DispatchMSPackChannel(HTTPXChannel):
153
+ """
154
+ A channel that sends a POST on the ìnvoke `endpoint`with an msgpack encoded request body.
155
+ """
149
156
  # constructor
150
157
 
151
158
  def __init__(self):
152
- super().__init__("dispatch-msgpack")
159
+ super().__init__()
153
160
 
154
161
  # override
155
162
 
156
- def set_address(self, address: Optional[ServiceAddress]):
163
+ def set_address(self, address: Optional[ChannelInstances]):
157
164
  ServiceManager.logger.info("channel %s got an address %s", self.name, address)
158
165
 
159
166
  super().set_address(address)
@@ -207,4 +214,3 @@ class DispatchMSPackChannel(HTTPXChannel):
207
214
 
208
215
  except Exception as e:
209
216
  raise ServiceException(f"msgpack exception: {e}") from e
210
-
@@ -9,7 +9,7 @@ import time
9
9
  from enum import Enum
10
10
  from typing import Any, Callable, Type, Optional
11
11
 
12
- from aspyx.di import Providers, ClassInstanceProvider, injectable, Environment, inject_environment, on_init
12
+ from aspyx.di import injectable, Environment, inject_environment, on_init
13
13
  from aspyx.reflection import Decorators, TypeDescriptor
14
14
 
15
15
 
@@ -31,7 +31,8 @@ def health_checks():
31
31
 
32
32
  def health_check(name="", cache = 0, fail_if_slower_than = 0):
33
33
  """
34
- Methods annotated with `@on_init` will be called when the instance is created."""
34
+ Methods annotated with `@on_init` will be called when the instance is created.
35
+ """
35
36
  def decorator(func):
36
37
  Decorators.add(func, health_check, name, cache, fail_if_slower_than)
37
38
  return func
@@ -39,6 +40,13 @@ def health_check(name="", cache = 0, fail_if_slower_than = 0):
39
40
  return decorator
40
41
 
41
42
  class HealthStatus(Enum):
43
+ """
44
+ A enum specifying the health status of a service. The values are:
45
+
46
+ - `OK` service is healthy
47
+ - `WARNING` service has some problems
48
+ - `CRITICAL` service is unhealthy
49
+ """
42
50
  OK = 1
43
51
  WARNING = 2
44
52
  ERROR = 3
@@ -49,6 +57,9 @@ class HealthStatus(Enum):
49
57
 
50
58
  @injectable()
51
59
  class HealthCheckManager:
60
+ """
61
+ The health manager is able to run all registered health checks and is able to return an overall status.
62
+ """
52
63
  logger = logging.getLogger("aspyx.service.health")
53
64
 
54
65
  # local classes
@@ -112,8 +123,8 @@ class HealthCheckManager:
112
123
  return result
113
124
 
114
125
  class Health:
115
- def __init__(self):
116
- self.status = HealthStatus.OK
126
+ def __init__(self, status: HealthStatus = HealthStatus.OK):
127
+ self.status = status
117
128
  self.results : list[HealthCheckManager.Result] = []
118
129
 
119
130
  def to_dict(self):
@@ -135,6 +146,11 @@ class HealthCheckManager:
135
146
  # check
136
147
 
137
148
  async def check(self) -> HealthCheckManager.Health:
149
+ """
150
+ run all registered health checks and return an overall result.
151
+ Returns: the overall result.
152
+
153
+ """
138
154
  self.logger.info("Checking health...")
139
155
 
140
156
  health = HealthCheckManager.Health()
@@ -15,9 +15,13 @@ from aspyx.di import on_init
15
15
  from .healthcheck import HealthCheckManager, HealthStatus
16
16
 
17
17
  from .server import Server
18
- from .service import ComponentRegistry, Channel, ServiceAddress, ServiceManager, ComponentDescriptor, Component, ChannelAddress
18
+ from .service import ComponentRegistry, Channel, ChannelInstances, ServiceManager, ComponentDescriptor, Component, ChannelAddress
19
19
 
20
20
  class ConsulComponentRegistry(ComponentRegistry):
21
+ """
22
+ A specialized registry using consul.
23
+ A polling mechanism is used to identify changes in the component health.
24
+ """
21
25
  # constructor
22
26
 
23
27
  def __init__(self, port: int, consul_url: str):
@@ -28,7 +32,7 @@ class ConsulComponentRegistry(ComponentRegistry):
28
32
  self.watchdog = None
29
33
  self.interval = 5
30
34
  self.last_index = {}
31
- self.component_addresses : dict[str, dict[str, ServiceAddress]] = {} # comp -> channel -> address
35
+ self.component_addresses : dict[str, dict[str, ChannelInstances]] = {} # comp -> channel -> address
32
36
  self.watch_channels : list[Channel] = []
33
37
 
34
38
  parsed = urlparse(consul_url)
@@ -36,7 +40,18 @@ class ConsulComponentRegistry(ComponentRegistry):
36
40
  self.consul_host = parsed.hostname
37
41
  self.consul_port = parsed.port
38
42
 
39
- def make_consul(self, host: str, port: int) -> consul.Consul:
43
+ def make_consul(self, host="", port="") -> consul.Consul:
44
+ """
45
+ create and return a consul instance
46
+
47
+ Args:
48
+ host: the host
49
+ port: the port
50
+
51
+ Returns:
52
+ a consul instance
53
+
54
+ """
40
55
  return consul.Consul(host=host, port=port)
41
56
 
42
57
  # lifecycle hooks
@@ -53,7 +68,7 @@ class ConsulComponentRegistry(ComponentRegistry):
53
68
  self.watchdog = threading.Thread(target=self.watch_consul, daemon=True)
54
69
  self.watchdog.start()
55
70
 
56
- def inform_channels(self, old_address: ServiceAddress, new_address: Optional[ServiceAddress]):
71
+ def inform_channels(self, old_address: ChannelInstances, new_address: Optional[ChannelInstances]):
57
72
  for channel in self.watch_channels:
58
73
  if channel.address is old_address:
59
74
  channel.set_address(new_address)
@@ -110,16 +125,14 @@ class ConsulComponentRegistry(ComponentRegistry):
110
125
  time.sleep(5)
111
126
 
112
127
  @abstractmethod
113
- def watch(self, channel: Channel):
128
+ def watch(self, channel: Channel) -> None:
114
129
  self.watch_channels.append(channel)
115
130
 
116
131
  #self.component_addresses[channel.component_descriptor.name] = {}
117
132
 
118
133
  # public
119
134
 
120
- def register_service(self, name, service_id, health: str, tags=None, meta=None):
121
- ip = "host.docker.internal" # TODO
122
-
135
+ def register_service(self, name, service_id, health: str, tags=None, meta=None) -> None:
123
136
  self.consul.agent.service.register(
124
137
  name=name,
125
138
  service_id=service_id,
@@ -128,13 +141,13 @@ class ConsulComponentRegistry(ComponentRegistry):
128
141
  tags=tags or [],
129
142
  meta=meta or {},
130
143
  check=consul.Check().http(
131
- url=f"http://{ip}:{self.port}{health}",
144
+ url=f"http://{self.ip}:{self.port}{health}",
132
145
  interval="10s",
133
146
  timeout="3s",
134
147
  deregister="5m")
135
148
  )
136
149
 
137
- def deregister(self, descriptor: ComponentDescriptor[Component]):
150
+ def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
138
151
  name = descriptor.name
139
152
 
140
153
  service_id = f"{self.ip}:{self.port}:{name}"
@@ -149,8 +162,8 @@ class ConsulComponentRegistry(ComponentRegistry):
149
162
 
150
163
  # private
151
164
 
152
- def fetch_addresses(self, component: str, wait=None) -> dict[str, ServiceAddress]:
153
- addresses : dict[str, ServiceAddress] = {} # channel name -> ServiceAddress
165
+ def fetch_addresses(self, component: str, wait=None) -> dict[str, ChannelInstances]:
166
+ addresses : dict[str, ChannelInstances] = {} # channel name -> ServiceAddress
154
167
 
155
168
  index, nodes = self.consul.health.service(component, passing=True, index=self.last_index.get(component, None), wait=wait)
156
169
  self.last_index[component] = index
@@ -170,7 +183,7 @@ class ConsulComponentRegistry(ComponentRegistry):
170
183
 
171
184
  address = addresses.get(channel, None)
172
185
  if address is None:
173
- address = ServiceAddress(component=component, channel=channel_name)
186
+ address = ChannelInstances(component=component, channel=channel_name)
174
187
  addresses[channel] = address
175
188
 
176
189
  address.urls.append(url)
@@ -200,7 +213,7 @@ class ConsulComponentRegistry(ComponentRegistry):
200
213
 
201
214
  self.register_service(name, id, descriptor.health, tags =["component"], meta={"channels": addresses})
202
215
 
203
- def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
216
+ def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
204
217
  component_addresses = self.component_addresses.get(descriptor.name, None)
205
218
  if component_addresses is None:
206
219
  component_addresses = self.fetch_addresses(descriptor.name)
@@ -1,3 +1,6 @@
1
+ """
2
+ rest channel implementation
3
+ """
1
4
  import inspect
2
5
  import re
3
6
  from dataclasses import is_dataclass, asdict
@@ -7,7 +10,7 @@ from typing import get_type_hints, TypeVar, Annotated, Callable, get_origin, get
7
10
  from pydantic import BaseModel
8
11
 
9
12
  from aspyx.reflection import DynamicProxy, Decorators
10
- from .service import RemoteServiceException, ServiceException, channel
13
+ from .service import ServiceException, channel
11
14
 
12
15
  T = TypeVar("T")
13
16
 
@@ -25,14 +28,26 @@ QueryParam = lambda t: Annotated[t, QueryParamMarker]
25
28
 
26
29
  # decorators
27
30
 
28
- def rest(url):
31
+ def rest(url=""):
32
+ """
33
+ mark service interfaces to add a url prefix
34
+
35
+ Args:
36
+ url: prefix that will be added to all urls
37
+ """
29
38
  def decorator(cls):
30
39
  Decorators.add(cls, rest, url)
31
40
 
32
41
  return cls
33
42
  return decorator
34
43
 
35
- def get(url):
44
+ def get(url: str):
45
+ """
46
+ methods marked with `get` will be executed by calling a http get request.
47
+
48
+ Args:
49
+ url: the url
50
+ """
36
51
  def decorator(cls):
37
52
  Decorators.add(cls, get, url)
38
53
 
@@ -40,7 +55,14 @@ def get(url):
40
55
  return decorator
41
56
 
42
57
 
43
- def post(url):
58
+ def post(url: str):
59
+ """
60
+ methods marked with `post` will be executed by calling a http get request.
61
+ The body parameter should be marked with `Body(<param>)`
62
+
63
+ Args:
64
+ url: the url
65
+ """
44
66
  def decorator(cls):
45
67
  Decorators.add(cls, post, url)
46
68
 
@@ -48,7 +70,13 @@ def post(url):
48
70
 
49
71
  return decorator
50
72
 
51
- def put(url):
73
+ def put(url: str):
74
+ """
75
+ methods marked with `put` will be executed by calling a http put request.
76
+
77
+ Args:
78
+ url: the url
79
+ """
52
80
  def decorator(cls):
53
81
  Decorators.add(cls, put, url)
54
82
 
@@ -56,7 +84,13 @@ def put(url):
56
84
 
57
85
  return decorator
58
86
 
59
- def delete(url):
87
+ def delete(url: str):
88
+ """
89
+ methods marked with `delete` will be executed by calling a http delete request.
90
+
91
+ Args:
92
+ url: the url
93
+ """
60
94
  def decorator(cls):
61
95
  Decorators.add(cls, delete, url)
62
96
 
@@ -64,7 +98,13 @@ def delete(url):
64
98
 
65
99
  return decorator
66
100
 
67
- def patch(url):
101
+ def patch(url: str):
102
+ """
103
+ methods marked with `patch` will be executed by calling a http patch request.
104
+
105
+ Args:
106
+ url: the url
107
+ """
68
108
  def decorator(cls):
69
109
  Decorators.add(cls, patch, url)
70
110
 
@@ -72,20 +112,22 @@ def patch(url):
72
112
 
73
113
  return decorator
74
114
 
75
-
76
-
77
-
78
115
  @channel("rest")
79
116
  class RestChannel(HTTPXChannel):
117
+ """
118
+ A rest channel executes http requests as specified by the corresponding decorators and annotations,
119
+ """
80
120
  __slots__ = [
81
121
  "signature",
82
122
  "url_template",
83
123
  "type",
124
+ "calls",
84
125
  "return_type",
85
126
  "path_param_names",
86
127
  "query_param_names",
87
128
  "body_param_name"
88
129
  ]
130
+
89
131
  # local class
90
132
 
91
133
  class Call:
@@ -133,7 +175,7 @@ class RestChannel(HTTPXChannel):
133
175
  # constructor
134
176
 
135
177
  def __init__(self):
136
- super().__init__("rest")
178
+ super().__init__()
137
179
 
138
180
  self.calls : dict[Callable, RestChannel.Call] = {}
139
181
 
@@ -196,4 +238,4 @@ class RestChannel(HTTPXChannel):
196
238
  raise ServiceException(
197
239
  f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
198
240
  except Exception as e:
199
- raise ServiceException(f"communication exception {e}") from e
241
+ raise ServiceException(f"communication exception {e}") from e
@@ -98,7 +98,7 @@ class TypeDeserializer:
98
98
  return deser_union
99
99
 
100
100
  if isinstance(typ, type) and issubclass(typ, BaseModel):
101
- return lambda v: typ.parse_obj(v)
101
+ return typ.parse_obj
102
102
 
103
103
  if is_dataclass(typ):
104
104
  field_deserializers = {f.name: TypeDeserializer(f.type) for f in fields(typ)}
@@ -122,4 +122,13 @@ class TypeDeserializer:
122
122
 
123
123
  @lru_cache(maxsize=512)
124
124
  def get_deserializer(typ):
125
+ """
126
+ return a function that is able to deserialize a value of the specified type
127
+
128
+ Args:
129
+ typ: the type
130
+
131
+ Returns:
132
+
133
+ """
125
134
  return TypeDeserializer(typ)
aspyx_service/server.py CHANGED
@@ -2,7 +2,6 @@
2
2
  FastAPI server implementation for the aspyx service framework.
3
3
  """
4
4
  import atexit
5
- import functools
6
5
  import inspect
7
6
  import threading
8
7
  from typing import Type, Optional, Callable
@@ -28,6 +27,9 @@ from .restchannel import get, post, put, delete, rest
28
27
 
29
28
 
30
29
  class FastAPIServer(Server):
30
+ """
31
+ A server utilizing fastapi framework.
32
+ """
31
33
  # constructor
32
34
 
33
35
  def __init__(self, host="0.0.0.0", port=8000, **kwargs):
@@ -41,7 +43,7 @@ class FastAPIServer(Server):
41
43
  self.component_registry: Optional[ComponentRegistry] = None
42
44
 
43
45
  self.router = APIRouter()
44
- self.fast_api = FastAPI(host=self.host, port=Server.port, debug=True)
46
+ self.fast_api = FastAPI()
45
47
 
46
48
  # cache
47
49
 
@@ -82,14 +84,21 @@ class FastAPIServer(Server):
82
84
  )
83
85
 
84
86
  def start(self):
85
- def run():
86
- uvicorn.run(self.fast_api, host=self.host, port=self.port, access_log=False)
87
+ """
88
+ start the fastapi server in a thread
89
+ """
90
+
91
+ config = uvicorn.Config(self.fast_api, host=self.host, port=self.port, log_level="info")
92
+ server = uvicorn.Server(config)
87
93
 
88
- self.server_thread = threading.Thread(target=run, daemon=True)
89
- self.server_thread.start()
94
+ thread = threading.Thread(target=server.run, daemon=True)
95
+ thread.start()
90
96
 
91
97
  print(f"server started on {self.host}:{self.port}")
92
98
 
99
+ return thread
100
+
101
+
93
102
  def get_deserializers(self, service: Type, method):
94
103
  deserializers = self.deserializers.get(method, None)
95
104
  if deserializers is None:
@@ -105,8 +114,8 @@ class FastAPIServer(Server):
105
114
 
106
115
  deserializers = self.get_deserializers(type, method)
107
116
 
108
- for i in range(0, len(args)):
109
- args[i] = deserializers[i](args[i])
117
+ for i, arg in enumerate(args):
118
+ args[i] = deserializers[i](arg)
110
119
 
111
120
  return args
112
121
 
@@ -181,6 +190,15 @@ class FastAPIServer(Server):
181
190
  self.router.get(url)(get_health_response)
182
191
 
183
192
  def boot(self, module_type: Type) -> Environment:
193
+ """
194
+ startup the service manager, DI framework and the fastapi server based on the supplied module
195
+
196
+ Args:
197
+ module_type: the module
198
+
199
+ Returns:
200
+
201
+ """
184
202
  # setup environment
185
203
 
186
204
  self.environment = Environment(module_type)
@@ -194,8 +212,8 @@ class FastAPIServer(Server):
194
212
  self.add_routes()
195
213
  self.fast_api.include_router(self.router)
196
214
 
197
- #for route in self.fast_api.routes:
198
- # print(f"{route.name}: {route.path} [{route.methods}]")
215
+ for route in self.fast_api.routes:
216
+ print(f"{route.name}: {route.path} [{route.methods}]")
199
217
 
200
218
  # start server thread
201
219
 
aspyx_service/service.py CHANGED
@@ -3,6 +3,7 @@ service management framework allowing for service discovery and transparent remo
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
+ import re
6
7
  import socket
7
8
  import logging
8
9
  import threading
@@ -24,11 +25,10 @@ class Service:
24
25
  """
25
26
  This is something like a 'tagging interface' for services.
26
27
  """
27
- pass
28
28
 
29
29
  class ComponentStatus(Enum):
30
30
  """
31
- A component is ij one of the following status:
31
+ A component is in one of the following statuses:
32
32
 
33
33
  - VIRGIN: just constructed
34
34
  - RUNNING: registered and up and running
@@ -41,7 +41,7 @@ class ComponentStatus(Enum):
41
41
  class Server(ABC):
42
42
  """
43
43
  A server is a central entity that boots a main module and initializes the ServiceManager.
44
- It also is the place where http servers get initialized,
44
+ It also is the place where http servers get initialized.
45
45
  """
46
46
  port = 0
47
47
 
@@ -64,6 +64,11 @@ class Server(ABC):
64
64
 
65
65
  @classmethod
66
66
  def get_local_ip(cls):
67
+ """
68
+ return the local ip address
69
+
70
+ Returns: the local ip address
71
+ """
67
72
  try:
68
73
  # create a dummy socket to an external address
69
74
 
@@ -72,8 +77,7 @@ class Server(ABC):
72
77
  ip = s.getsockname()[0]
73
78
  s.close()
74
79
 
75
- raise Exception("TODO")
76
- #return ip
80
+ return ip
77
81
  except Exception:
78
82
  return "127.0.0.1" # Fallback
79
83
 
@@ -98,24 +102,44 @@ class Component(Service):
98
102
  This is the base class for components.
99
103
  """
100
104
  @abstractmethod
101
- def startup(self):
102
- pass
105
+ def startup(self) -> None:
106
+ """
107
+ startup callback
108
+ """
103
109
 
104
110
  @abstractmethod
105
- def shutdown(self):
106
- pass
111
+ def shutdown(self)-> None:
112
+ """
113
+ shutdown callback
114
+ """
107
115
 
108
116
  @abstractmethod
109
117
  def get_addresses(self, port: int) -> list[ChannelAddress]:
110
- pass
118
+ """
119
+ returns a list of channel addresses that expose this components services.
120
+
121
+ Args:
122
+ port: the port of a server hosting this component
123
+
124
+ Returns:
125
+ list of channel addresses
126
+ """
111
127
 
112
128
  @abstractmethod
113
129
  def get_status(self) -> ComponentStatus:
114
- pass
130
+ """
131
+ return the component status callback
132
+
133
+ Returns: the component status
134
+ """
115
135
 
116
136
  @abstractmethod
117
137
  async def get_health(self) -> HealthCheckManager.Health:
118
- pass
138
+ """
139
+ return the component health
140
+
141
+ Returns: the component health
142
+ """
119
143
 
120
144
  class AbstractComponent(Component, ABC):
121
145
  """
@@ -126,18 +150,33 @@ class AbstractComponent(Component, ABC):
126
150
  def __init__(self):
127
151
  self.status = ComponentStatus.VIRGIN
128
152
 
129
- def startup(self):
153
+ def startup(self) -> None:
130
154
  self.status = ComponentStatus.RUNNING
131
155
 
132
- def shutdown(self):
156
+ def shutdown(self) -> None:
133
157
  self.status = ComponentStatus.STOPPED
134
158
 
135
- def get_status(self):
159
+ def get_status(self) -> ComponentStatus:
136
160
  return self.status
137
161
 
162
+ def to_snake_case(name: str) -> str:
163
+ return re.sub(r'(?<!^)(?=[A-Z])', '-', name).lower()
164
+
138
165
  def component(name = "", description="", services: list[Type] = []):
166
+ """
167
+ decorates component interfaces
168
+
169
+ Args:
170
+ name: the component name. If empty the class name converted to snake-case is used
171
+ description: optional description
172
+ services: the list of hosted services
173
+ """
139
174
  def decorator(cls):
140
- Decorators.add(cls, component, name, description, services)
175
+ component_name = name
176
+ if component_name == "":
177
+ component_name = to_snake_case(cls.__name__)
178
+
179
+ Decorators.add(cls, component, component_name, description, services)
141
180
 
142
181
  ServiceManager.register_component(cls, services)
143
182
 
@@ -148,8 +187,19 @@ def component(name = "", description="", services: list[Type] = []):
148
187
  return decorator
149
188
 
150
189
  def service(name = "", description = ""):
190
+ """
191
+ decorates service interfaces
192
+
193
+ Args:
194
+ name: the service name. If empty the class name converted to snake case is used
195
+ description: optional description
196
+ """
151
197
  def decorator(cls):
152
- Decorators.add(cls, service, name, description)
198
+ service_name = name
199
+ if service_name == "":
200
+ service_name = to_snake_case(cls.__name__)
201
+
202
+ Decorators.add(cls, service, service_name, description)
153
203
 
154
204
  Providers.register(ServiceInstanceProvider(cls))
155
205
 
@@ -157,16 +207,24 @@ def service(name = "", description = ""):
157
207
 
158
208
  return decorator
159
209
 
210
+ def health(endpoint = ""):
211
+ """
212
+ specifies the health endpoint that will return the component health
160
213
 
161
- def health(name = ""):
214
+ Args:
215
+ endpoint: the health endpoint
216
+ """
162
217
  def decorator(cls):
163
- Decorators.add(cls, health, name)
218
+ Decorators.add(cls, health, endpoint)
164
219
 
165
220
  return cls
166
221
 
167
222
  return decorator
168
223
 
169
224
  def implementation():
225
+ """
226
+ decorates service or component implementations.
227
+ """
170
228
  def decorator(cls):
171
229
  Decorators.add(cls, implementation)
172
230
 
@@ -299,7 +357,14 @@ class ComponentDescriptor(BaseDescriptor[T]):
299
357
  # a resolved channel address
300
358
 
301
359
  @dataclass()
302
- class ServiceAddress:
360
+ class ChannelInstances:
361
+ """
362
+ a resolved channel address containing:
363
+
364
+ - component: the component name
365
+ - channel: the channel name
366
+ - urls: list of URLs
367
+ """
303
368
  component: str
304
369
  channel: str
305
370
  urls: list[str]
@@ -312,18 +377,29 @@ class ServiceAddress:
312
377
  self.urls : list[str] = sorted(urls)
313
378
 
314
379
  class ServiceException(Exception):
315
- pass
380
+ """
381
+ base class for service exceptions
382
+ """
316
383
 
317
384
  class LocalServiceException(ServiceException):
318
- pass
385
+ """
386
+ base class for service exceptions occurring locally
387
+ """
319
388
 
320
389
  class ServiceCommunicationException(ServiceException):
321
- pass
390
+ """
391
+ base class for service exceptions thrown by remoting errors
392
+ """
322
393
 
323
394
  class RemoteServiceException(ServiceException):
324
- pass
395
+ """
396
+ base class for service exceptions occurring on the server side
397
+ """
325
398
 
326
399
  class Channel(DynamicProxy.InvocationHandler, ABC):
400
+ """
401
+ A channel is a dynamic proxy invocation handler and transparently takes care of remoting.
402
+ """
327
403
  __slots__ = [
328
404
  "name",
329
405
  "component_descriptor",
@@ -331,11 +407,25 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
331
407
  ]
332
408
 
333
409
  class URLSelector:
410
+ """
411
+ a url selector retrieves a URL for the next remoting call.
412
+ """
334
413
  @abstractmethod
335
414
  def get(self, urls: list[str]) -> str:
336
- pass
415
+ """
416
+ return the next URL given a list of possible URLS
417
+
418
+ Args:
419
+ urls: list of possible URLS
420
+
421
+ Returns:
422
+ a URL
423
+ """
337
424
 
338
425
  class FirstURLSelector(URLSelector):
426
+ """
427
+ a url selector always retrieving the first URL given a list of possible URLS
428
+ """
339
429
  def get(self, urls: list[str]) -> str:
340
430
  if len(urls) == 0:
341
431
  raise ServiceCommunicationException("no known url")
@@ -343,6 +433,9 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
343
433
  return urls[0]
344
434
 
345
435
  class RoundRobinURLSelector(URLSelector):
436
+ """
437
+ a url selector that picks urls sequentially given a list of possible URLS
438
+ """
346
439
  def __init__(self):
347
440
  self.index = 0
348
441
 
@@ -358,59 +451,90 @@ class Channel(DynamicProxy.InvocationHandler, ABC):
358
451
 
359
452
  # constructor
360
453
 
361
- def __init__(self, name: str):
362
- self.name = name
454
+ def __init__(self):
455
+ self.name = Decorators.get_decorator(type(self), channel).args[0]
363
456
  self.component_descriptor : Optional[ComponentDescriptor] = None
364
- self.address: Optional[ServiceAddress] = None
365
- self.url_provider : Channel.URLSelector = Channel.FirstURLSelector()
457
+ self.address: Optional[ChannelInstances] = None
458
+ self.url_selector : Channel.URLSelector = Channel.FirstURLSelector()
366
459
 
367
460
  # public
368
461
 
369
462
  def customize(self):
370
463
  pass
371
464
 
372
- def select_round_robin(self):
373
- self.url_provider = Channel.RoundRobinURLSelector()
465
+ def select_round_robin(self) -> None:
466
+ """
467
+ enable round robin
468
+ """
469
+ self.url_selector = Channel.RoundRobinURLSelector()
374
470
 
375
471
  def select_first_url(self):
376
- self.url_provider = Channel.FirstURLSelector()
472
+ """
473
+ pick the first URL
474
+ """
475
+ self.url_selector = Channel.FirstURLSelector()
377
476
 
378
477
  def get_url(self) -> str:
379
- return self.url_provider.get(self.address.urls)
478
+ return self.url_selector.get(self.address.urls)
380
479
 
381
- def set_address(self, address: Optional[ServiceAddress]):
480
+ def set_address(self, address: Optional[ChannelInstances]):
382
481
  self.address = address
383
482
 
384
- def setup(self, component_descriptor: ComponentDescriptor, address: ServiceAddress):
483
+ def setup(self, component_descriptor: ComponentDescriptor, address: ChannelInstances):
385
484
  self.component_descriptor = component_descriptor
386
485
  self.address = address
387
486
 
388
487
 
389
488
  class ComponentRegistry:
489
+ """
490
+ A component registry keeps track of components including their health
491
+ """
390
492
  @abstractmethod
391
- def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
392
- pass
493
+ def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]) -> None:
494
+ """
495
+ register a component to the registry
496
+ Args:
497
+ descriptor: the descriptor
498
+ addresses: list of addresses
499
+ """
393
500
 
394
501
  @abstractmethod
395
- def deregister(self, descriptor: ComponentDescriptor[Component]):
396
- pass
502
+ def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
503
+ """
504
+ deregister a component from the registry
505
+ Args:
506
+ descriptor: the component descriptor
507
+ """
397
508
 
398
509
  @abstractmethod
399
- def watch(self, channel: Channel):
400
- pass
510
+ def watch(self, channel: Channel) -> None:
511
+ """
512
+ remember the passed channel and keep it informed about address changes
513
+ Args:
514
+ channel: a channel
515
+ """
401
516
 
402
517
  @abstractmethod
403
- def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
404
- pass
518
+ def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
519
+ """
520
+ return a list of addresses that can be used to call services belonging to this component
521
+
522
+ Args:
523
+ descriptor: the component descriptor
524
+
525
+ Returns:
526
+ list of channel instances
527
+ """
405
528
 
406
529
  def map_health(self, health: HealthCheckManager.Health) -> int:
407
530
  return 200
408
531
 
409
- def shutdown(self):
410
- pass
411
532
 
412
533
  @injectable()
413
534
  class ChannelManager:
535
+ """
536
+ Internal factory for channels.
537
+ """
414
538
  factories: dict[str, Type] = {}
415
539
 
416
540
  @classmethod
@@ -432,7 +556,7 @@ class ChannelManager:
432
556
 
433
557
  # public
434
558
 
435
- def make(self, name: str, descriptor: ComponentDescriptor, address: ServiceAddress) -> Channel:
559
+ def make(self, name: str, descriptor: ComponentDescriptor, address: ChannelInstances) -> Channel:
436
560
  ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
437
561
 
438
562
  result = self.environment.get(self.factories.get(name))
@@ -441,7 +565,13 @@ class ChannelManager:
441
565
 
442
566
  return result
443
567
 
444
- def channel(name):
568
+ def channel(name: str):
569
+ """
570
+ this decorator is used to mark channel implementations.
571
+
572
+ Args:
573
+ name: the channel name
574
+ """
445
575
  def decorator(cls):
446
576
  Decorators.add(cls, channel, name)
447
577
 
@@ -460,6 +590,9 @@ class TypeAndChannel:
460
590
 
461
591
  @injectable()
462
592
  class ServiceManager:
593
+ """
594
+ Central class that manages services and components and is able to return proxies.
595
+ """
463
596
  # class property
464
597
 
465
598
  logger = logging.getLogger("aspyx.service") # __name__ = module name
@@ -587,7 +720,7 @@ class ServiceManager:
587
720
 
588
721
  # public
589
722
 
590
- def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") -> ServiceAddress:
723
+ def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") -> ChannelInstances:
591
724
  addresses = self.component_registry.get_addresses(component_descriptor) # component, channel + urls
592
725
  address = next((address for address in addresses if address.channel == preferred_channel), None)
593
726
  if address is None:
@@ -600,6 +733,16 @@ class ServiceManager:
600
733
  return address
601
734
 
602
735
  def get_service(self, service_type: Type[T], preferred_channel="") -> T:
736
+ """
737
+ return a service proxy given a service type and preferred channel name
738
+
739
+ Args:
740
+ service_type: the service type
741
+ preferred_channel: the preferred channel name
742
+
743
+ Returns:
744
+ the proxy
745
+ """
603
746
  service_descriptor = ServiceManager.get_descriptor(service_type)
604
747
  component_descriptor = service_descriptor.get_component_descriptor()
605
748
 
@@ -688,7 +831,7 @@ class LocalChannel(Channel):
688
831
  # constructor
689
832
 
690
833
  def __init__(self, manager: ServiceManager):
691
- super().__init__("local")
834
+ super().__init__()
692
835
 
693
836
  self.manager = manager
694
837
  self.component = component
@@ -717,16 +860,16 @@ class LocalComponentRegistry(ComponentRegistry):
717
860
 
718
861
  # implement
719
862
 
720
- def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
863
+ def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]) -> None:
721
864
  self.component_channels[descriptor] = addresses
722
865
 
723
- def deregister(self, descriptor: ComponentDescriptor[Component]):
866
+ def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
724
867
  pass
725
868
 
726
- def watch(self, channel: Channel):
869
+ def watch(self, channel: Channel) -> None:
727
870
  pass
728
871
 
729
- def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
872
+ def get_addresses(self, descriptor: ComponentDescriptor) -> list[ChannelInstances]:
730
873
  return self.component_channels.get(descriptor, [])
731
874
 
732
875
  def inject_service(preferred_channel=""):
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aspyx_service
3
- Version: 0.9.0
4
- Summary: Service framework on top of aspyx
3
+ Version: 0.10.0
4
+ Summary: Aspyx Service framework
5
5
  Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
6
6
  License: MIT License
7
7
 
@@ -26,11 +26,12 @@ License: MIT License
26
26
  SOFTWARE.
27
27
  License-File: LICENSE
28
28
  Requires-Python: >=3.9
29
- Requires-Dist: aspyx>=1.4.1
29
+ Requires-Dist: aspyx>=1.5.0
30
30
  Requires-Dist: fastapi~=0.115.13
31
31
  Requires-Dist: httpx~=0.28.1
32
32
  Requires-Dist: msgpack~=1.1.1
33
33
  Requires-Dist: python-consul2~=0.1.5
34
+ Requires-Dist: uvicorn[standard]
34
35
  Description-Content-Type: text/markdown
35
36
 
36
37
  aspyx-service
@@ -0,0 +1,12 @@
1
+ aspyx_service/__init__.py,sha256=6t24VPrSCG83EAvYlqCKdEcEbyCY3vrSb5GoAx01Ymg,1662
2
+ aspyx_service/channels.py,sha256=HZFkbZq0Q1DNpOauuPCMOEh7UlpcisN8_gQhL66zS-0,7718
3
+ aspyx_service/healthcheck.py,sha256=8ZPSkAx6ypoYaxDMkJT_MtL2pEN2LcUAishAWPCy-3I,5624
4
+ aspyx_service/registries.py,sha256=5sU9eFT_D33sAgrXyYSd9fAa8xNmFRJ6CmZGp8cQztY,7712
5
+ aspyx_service/restchannel.py,sha256=IbspE-hrzkpPLgBzuNncagjjGZnLVPX9jtT1PKRNenw,6453
6
+ aspyx_service/serialization.py,sha256=T6JDURwYd7V4240j1BnNsFKdyiMgePCsCwKa9rf8hq4,3779
7
+ aspyx_service/server.py,sha256=DA4bfTzYbpPrfqgddbyvTqr80Ehk9tuoqFfuo59ppN4,7118
8
+ aspyx_service/service.py,sha256=T7rZ8EPEzOusP_xOk-7KwTQfX_iJQ291XDL_ieKa_d8,25148
9
+ aspyx_service-0.10.0.dist-info/METADATA,sha256=9bVUDpfMPvHUr_NfWmOw4EYHA0rw8m84niRWxnhmslk,1674
10
+ aspyx_service-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ aspyx_service-0.10.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
12
+ aspyx_service-0.10.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- aspyx_service/__init__.py,sha256=5i5YqVKkhUwLsEGsP2YBRxyvavjueYsReDer6zst2zs,1570
2
- aspyx_service/channels.py,sha256=TD6uePNkHsbPSZqio0eaJYXbRMXWUH47TsGkN7_SO8E,7455
3
- aspyx_service/healthcheck.py,sha256=aXnET-ffaXi1YHSX5M8BDN3R4ySYSYbUmLtmEpiNBd0,5168
4
- aspyx_service/registries.py,sha256=DZy0T1wz5c7aoIQuH3wGOhktHbYAzCp_zEAzl4G-CWs,7395
5
- aspyx_service/restchannel.py,sha256=j2ihQF5lqeUL9IeDiL79WYhQj75BYQrvCT6-ttNf268,5455
6
- aspyx_service/serialization.py,sha256=19U-K-SUbEgrOCDHo5gV7m54LbHo40bjNobCuJyHcoc,3648
7
- aspyx_service/server.py,sha256=UfBWDmzfeAoYn1RyoUXLRmcLt78gf0EvSk6CQpMZEt0,6824
8
- aspyx_service/service.py,sha256=B60IlwmAqONG3K__Ic2X9WxEC6RYSMnoUcBryz6uYpQ,21123
9
- aspyx_service-0.9.0.dist-info/METADATA,sha256=aDTiBsHcFRVgWDumfmtXYUzWXDyjADVmMyT4CwX3EvM,1650
10
- aspyx_service-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- aspyx_service-0.9.0.dist-info/licenses/LICENSE,sha256=n4jfx_MNj7cBtPhhI7MCoB_K35cj1icP9yJ4Rh4vlvY,1070
12
- aspyx_service-0.9.0.dist-info/RECORD,,