mellifera 0.2.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.
mellifera/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from mellifera.executors import TrioExecutor, HAS_NSMAINTHREAD
2
+
3
+ if HAS_NSMAINTHREAD:
4
+ from mellifera.executors import NSMainThreadExecutor
5
+
6
+ from mellifera.orchestrator import Orchestrator
7
+ from mellifera.service import Service, expose
8
+ from mellifera.services import TrioService, NSMainThreadService
mellifera/executor.py ADDED
@@ -0,0 +1,61 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from mellifera.service import Service
4
+
5
+ class Executor(ABC):
6
+
7
+ @abstractmethod
8
+ def run_threadsafe(self, f, *args, **kwargs):
9
+ """Run function f with args and kwargs inside executor in a threadsafe matter
10
+
11
+ This function is threadsafe.
12
+ """
13
+ ...
14
+
15
+ @abstractmethod
16
+ def register_service(self, service: Service, name: str) -> None:
17
+ """Register `service` under `name`
18
+
19
+ Only makes the service known, does not automatically start it
20
+ """
21
+ ...
22
+
23
+ @abstractmethod
24
+ def start_service(self, service: Service) -> None:
25
+ """Start `service` eventually
26
+
27
+ Idempotent
28
+ """
29
+ ...
30
+
31
+ @abstractmethod
32
+ def stop_service(self, service: Service) -> None:
33
+ """Stop `service` eventually
34
+
35
+ Idempotent
36
+ """
37
+ ...
38
+
39
+ @abstractmethod
40
+ def stop_all(self) -> None:
41
+ """Stops all services
42
+
43
+ Idempotent
44
+ """
45
+ ...
46
+
47
+ @abstractmethod
48
+ def start_sync(self) -> None:
49
+ """Start the executor.
50
+
51
+ Returns after the executor is started (in a different thread or process)
52
+ """
53
+ ...
54
+
55
+ @abstractmethod
56
+ def run_sync(self) -> None:
57
+ """Run the executor
58
+
59
+ Returns after the executor is done
60
+ """
61
+ ...
@@ -0,0 +1,6 @@
1
+ from mellifera.executors.trio import TrioExecutor
2
+ try:
3
+ from mellifera.executors.nsmainthread import NSMainThreadExecutor
4
+ HAS_NSMAINTHREAD = True
5
+ except ModuleNotFoundError:
6
+ HAS_NSMAINTHREAD = False
@@ -0,0 +1,70 @@
1
+ try:
2
+ from Foundation import NSThread
3
+ from libdispatch import dispatch_async, dispatch_sync, dispatch_get_main_queue
4
+ except ModuleNotFoundError:
5
+ raise ModuleNotFoundError("To use mellifera.orchestrators.nsmainthread you need to have pyobjc installed and run on macos")
6
+
7
+ from types import SimpleNamespace
8
+
9
+ from mellifera.orchestrator import Orchestrator, threadsafe
10
+ from mellifera.services.nsmainthread import NSMainThreadService
11
+ from mellifera.service import Service
12
+ from mellifera.executor import Executor
13
+
14
+ from mellifera.service import ServiceState
15
+
16
+ class NSMainThreadExecutor(Executor):
17
+ requires_run = True
18
+
19
+ def __init__(self, orchestrator) -> None:
20
+ self.orchestrator = orchestrator
21
+ self.service = None
22
+
23
+ def run_threadsafe(self, f, *args, **kwargs):
24
+ if NSThread.isMainThread():
25
+ return f(*args, **kwargs)
26
+ else:
27
+
28
+ def closure():
29
+ return f(*args, **kwargs)
30
+
31
+ return dispatch_sync(dispatch_get_main_queue(), closure)
32
+
33
+ @threadsafe
34
+ def register_service(self, service: Service, name: str) -> None:
35
+ assert isinstance(service, NSMainThreadService)
36
+ if self.service is not None:
37
+ raise ValueError(
38
+ "MainThreadOrchestrator has multiple services to manage, can only orchestrate a single MainThreadService"
39
+ )
40
+ self.service = service
41
+ service.register(self.orchestrator, self, name)
42
+
43
+
44
+ @threadsafe
45
+ def start_service(self, service: Service) -> None:
46
+ if self.service is not None and self.service != service:
47
+ raise ValueError(f"Can only start a single service, already starting {self.service}")
48
+ if self.service is None:
49
+ self.service = service
50
+ service.state = ServiceState.WILL_BE_STARTED
51
+
52
+ @threadsafe
53
+ def stop_service(self, service: Service) -> None:
54
+ assert isinstance(service, NSMainThreadService)
55
+ service.stop()
56
+
57
+ @threadsafe
58
+ def stop_all(self) -> None:
59
+ self.service.stop()
60
+
61
+ def start_sync(self) -> None:
62
+ raise NotImplementedError()
63
+
64
+ def run_sync(self) -> None:
65
+ if self.service:
66
+ try:
67
+ self.service.init_sync()
68
+ self.service.run_sync()
69
+ finally:
70
+ self.service.finalize_sync()
@@ -0,0 +1,168 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ import trio
4
+ import trio.lowlevel
5
+ import threading
6
+ import inspect
7
+ import logging
8
+ from queue import Queue
9
+ from types import SimpleNamespace
10
+
11
+ from mellifera.services.trio import TrioService, ServiceState
12
+ from mellifera.orchestrator import Orchestrator, threadsafe
13
+ from mellifera.service import Service
14
+ from mellifera.executor import Executor
15
+
16
+
17
+ class TrioExecutor(Executor):
18
+ requires_run = False
19
+
20
+ def __init__(self, orchestrator) -> None:
21
+ self.orchestrator = orchestrator
22
+ self.trio_token = None
23
+ self.services = []
24
+ self.logger = logging.getLogger("mellifera.executors.TrioExecutor")
25
+
26
+ self.nursery = None
27
+ self.to_start = []
28
+ self._thread = None
29
+ self._lock = threading.RLock()
30
+ self.stopped = trio.Event()
31
+
32
+ self.write_channel, self.read_channel = trio.open_memory_channel(100)
33
+
34
+ def run_threadsafe(self, f, *args, **kwargs):
35
+ self.logger.info(f"run_threadsafe {f.__name__}")
36
+
37
+ if inspect.iscoroutinefunction(f):
38
+ async def closure():
39
+ return await f(*args, **kwargs)
40
+ is_async = True
41
+ else:
42
+ def closure():
43
+ return f(*args, **kwargs)
44
+ is_async = False
45
+
46
+ if trio.lowlevel.in_trio_run():
47
+ if is_async:
48
+ try:
49
+ return self.write_channel.send_nowait(closure)
50
+ except trio.BrokenResourceError as e:
51
+ self.logger.info("broken ressource error")
52
+ raise e
53
+
54
+ else:
55
+ return closure()
56
+ else:
57
+ # Orchestrator didn't start yet, use locking for synchronisation
58
+ if not self._thread:
59
+ with self._lock:
60
+ if is_async:
61
+ self.logger.info(f"run_threadsafe {f.__name__} with lock, trio.run")
62
+ return trio.run(closure)
63
+ else:
64
+ self.logger.info(f"run_threadsafe {f.__name__} with lock, direct")
65
+ return closure()
66
+ else:
67
+ if self.trio_token:
68
+ if is_async:
69
+ self.logger.info(f"run_threadsafe {f.__name__} with trio_token, from_thread.run")
70
+ return trio.from_thread.run(closure, trio_token=self.trio_token)
71
+ else:
72
+ self.logger.info(f"run_threadsafe {f.__name__} with trio_token, from_thread.run_sync")
73
+ return trio.from_thread.run_sync(
74
+ closure, trio_token=self.trio_token
75
+ )
76
+ else:
77
+ raise ValueError(
78
+ "Calling TrioOrchestrator.run_in_thread but trio_token is None"
79
+ )
80
+
81
+ @threadsafe
82
+ def register_service(self, service: Service, name: str) -> None:
83
+ assert isinstance(service, TrioService)
84
+ service.register(self.orchestrator, self, name)
85
+ self.services.append(service)
86
+
87
+ @threadsafe
88
+ async def start_service(self, service):
89
+ if self.stopped.is_set():
90
+ raise ValueError("Trying to start a service when executor is already stopped")
91
+ if service.state == ServiceState.CONSTRUCTED or service.state == ServiceState.WILL_BE_STARTED:
92
+ if service.state == ServiceState.CONSTRUCTED:
93
+ service.state = ServiceState.WILL_BE_STARTED
94
+ self.services.append(service)
95
+ if self.nursery:
96
+ self.logger.info(f"starting running service {service.name}")
97
+ self.nursery.start_soon(self.run_service, service)
98
+ else:
99
+ self.to_start.append(service)
100
+ else:
101
+ raise ValueError(f"Service is in state {service.state}")
102
+
103
+ @threadsafe
104
+ async def stop_service(self, service):
105
+ try:
106
+ with trio.fail_after(5):
107
+ await service.stop()
108
+ except trio.TooSlowError:
109
+ with trio.move_on_after(10):
110
+ await service.cancel()
111
+
112
+ @threadsafe
113
+ async def stop_all(self):
114
+ for service in self.services:
115
+ self.stop_service(service)
116
+ await self.write_channel.aclose()
117
+
118
+ async def run_service(self, service):
119
+ self.logger.info(f"Running service {service.name}")
120
+ try:
121
+ self.logger.info(f"Initializing service {service.name}")
122
+ await service.init()
123
+ self.logger.info(f"Initialized service {service.name}")
124
+ self.logger.info(f"Running service {service.name}")
125
+ await service.run()
126
+ self.logger.info(f"Running service {service.name} Done")
127
+ finally:
128
+ self.logger.info(f"Finalizing service {service.name}")
129
+ await service.finalize()
130
+ self.logger.info(f"Finalizing service {service.name} Done")
131
+
132
+ async def start(self, queue: Queue) -> None:
133
+ trio_token = trio.lowlevel.current_trio_token()
134
+ queue.put(trio_token)
135
+ await self.run()
136
+
137
+ async def run(self) -> None:
138
+ async with trio.open_nursery() as nursery:
139
+ self.nursery = nursery
140
+ for service in self.to_start:
141
+ nursery.start_soon(self.run_service, service)
142
+ self.to_start = []
143
+
144
+ try:
145
+ async with self.read_channel:
146
+ async for async_function in self.read_channel:
147
+ await async_function()
148
+ except trio.ClosedResourceError:
149
+ pass
150
+
151
+ def start_sync(self) -> None:
152
+ try:
153
+ queue = Queue()
154
+ self._thread = threading.Thread(target=trio.run, args=(self.start, queue))
155
+ self._thread.start()
156
+ value = queue.get()
157
+ if isinstance(value, Exception):
158
+ raise value
159
+ else:
160
+ self.trio_token = value
161
+ except BaseException as e:
162
+ # self.logger.error("Exception occured during start_sync", exc_info=True)
163
+ # for name, service in self._services.items():
164
+ # self.logger.error(f"Service {name} is in state {service.state}")
165
+ raise e
166
+
167
+ def run_sync(self) -> None:
168
+ trio.run(self.run)
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from types import SimpleNamespace
5
+ import functools
6
+ import logging
7
+
8
+ from typing import TYPE_CHECKING, Any, Optional
9
+ import threading
10
+
11
+ from mellifera.executor import Executor
12
+ from mellifera.services import TrioService, NSMainThreadService
13
+
14
+ if TYPE_CHECKING:
15
+ from mellifera.service import Service
16
+
17
+ def threadsafe(f):
18
+ @functools.wraps(f)
19
+ def inner(self, *args, **kwargs):
20
+ return self.run_threadsafe(f, self, *args, **kwargs)
21
+
22
+ return inner
23
+
24
+ class Orchestrator:
25
+
26
+ def __init__(self, stop_on_finish=True) -> None:
27
+ self.logger = logging.getLogger("calsiprovis.orchestrator.ParentOrchestrator")
28
+ self.services = SimpleNamespace()
29
+
30
+ from mellifera.executors import TrioExecutor, HAS_NSMAINTHREAD
31
+ if HAS_NSMAINTHREAD:
32
+ from mellifera.executors import NSMainThreadExecutor
33
+
34
+
35
+ self.executors: dict[str, Executor] = {"trio": TrioExecutor(self)}
36
+
37
+ if HAS_NSMAINTHREAD:
38
+ self.executors["ns"] = NSMainThreadExecutor(self)
39
+
40
+ self.lock = threading.RLock()
41
+
42
+
43
+ def start_service(self, service: Service | str) -> None:
44
+ if isinstance(service, str):
45
+ service = self.get_service(service)
46
+ match service:
47
+ case TrioService():
48
+ self.executors["trio"].start_service(service)
49
+ case _:
50
+ raise ValueError(f"Cannot start service of type {type(service)}")
51
+
52
+ def stop_service(self, service: Service) -> None:
53
+ match service:
54
+ case TrioService():
55
+ self.executors["trio"].stop_service(service)
56
+ case NSMainThreadService():
57
+ self.executors["ns"].stop_service(service)
58
+ case _:
59
+ raise ValueError(f"Cannot start service of type {type(service)}")
60
+
61
+ def register_service(self, service: Service, name: str) -> None:
62
+ with self.lock:
63
+ match service:
64
+ case TrioService():
65
+ self.executors["trio"].register_service(service, name)
66
+ case NSMainThreadService():
67
+ if "ns" in self.executors:
68
+ self.executors["ns"].register_service(service, name)
69
+ else:
70
+ raise ValueError("Cannot handle NSMainThreadService, as NSMainThreadExecutor is not available")
71
+ case _:
72
+ raise ValueError(f"Cannot handle service of type {type(service)}")
73
+ setattr(self.services, name, service)
74
+
75
+ def get_service(self, name: str) -> Service:
76
+ with self.lock:
77
+ if hasattr(self.services, name):
78
+ return getattr(self.services, name)
79
+ else:
80
+ raise ValueError(f"Service with name `{name}` not found")
81
+
82
+ def run(self):
83
+ if "ns" in self.executors:
84
+ self.executors["trio"].start_sync()
85
+ self.executors["ns"].run_sync()
86
+ else:
87
+ self.executors["trio"].run_sync()
88
+
89
+ def stop_all(self):
90
+ for executor in self.executors.values():
91
+ executor.stop_all()
92
+
mellifera/service.py ADDED
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from mellifera.orchestrator import Orchestrator
7
+ from logging import Logger
8
+ from mellifera.executor import Executor
9
+
10
+ import trio
11
+ from typing import final
12
+ import logging
13
+ from functools import wraps
14
+ from enum import Enum
15
+
16
+
17
+
18
+ class ServiceError(Exception):
19
+ pass
20
+
21
+
22
+ class ServiceState(Enum):
23
+ NONE = 0
24
+ CONSTRUCTED = 1
25
+ WILL_BE_STARTED = 2
26
+ INITIATING = 3
27
+ INITIATED = 4
28
+ RUNNING = 5
29
+ STOPPING = 6
30
+ STOPPED = 7
31
+ SHUTDOWN_STARTED = 8
32
+ SHUTDOWN = 9
33
+
34
+
35
+ def expose(f):
36
+ @wraps(f)
37
+ def _inner(service, *args, **kwargs):
38
+ if (not service.state == ServiceState.INITIATED) and (
39
+ not service.state == ServiceState.RUNNING
40
+ ):
41
+ raise RuntimeError(
42
+ f"Service `{service.name}` not running, but is in state {service.state}"
43
+ )
44
+ return service.orchestrator.run_threadsafe(f, service, *args, **kwargs)
45
+
46
+ return _inner
47
+
48
+
49
+ class Service:
50
+
51
+ orchestrator: Orchestrator
52
+ executor: Executor
53
+ logger: Logger
54
+ name: str
55
+
56
+ def __init__(self) -> None:
57
+ self._state = ServiceState.NONE
58
+
59
+ self.initiated_event = trio.Event()
60
+ self.started_event = trio.Event()
61
+ self.stopped_event = trio.Event()
62
+ self.shutdown_event = trio.Event()
63
+
64
+
65
+ @property
66
+ def initiated(self):
67
+ return self.initiated_event.wait()
68
+
69
+ @property
70
+ def started(self):
71
+ return self.started_event.wait()
72
+
73
+ @property
74
+ def stopped(self):
75
+ return self.stopped_event.wait()
76
+
77
+ @property
78
+ def shutdown(self):
79
+ return self.shutdown_event.wait()
80
+
81
+ @property
82
+ def state(self) -> ServiceState:
83
+ return self._state
84
+
85
+ @state.setter
86
+ def state(self, state: ServiceState) -> None:
87
+ match state:
88
+ case ServiceState.NONE:
89
+ assert False
90
+ case ServiceState.CONSTRUCTED:
91
+ if self._state != ServiceState.NONE:
92
+ raise ServiceError(
93
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
94
+ )
95
+ self._state = state
96
+ case ServiceState.WILL_BE_STARTED:
97
+ if self._state != ServiceState.CONSTRUCTED:
98
+ raise ServiceError(
99
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
100
+ )
101
+ self._state = state
102
+ case ServiceState.INITIATING:
103
+ if self._state != ServiceState.WILL_BE_STARTED:
104
+ raise ServiceError(
105
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
106
+ )
107
+ self._state = state
108
+ self.logger.debug("Initiating")
109
+ case ServiceState.INITIATED:
110
+ if self._state != ServiceState.INITIATING:
111
+ raise ServiceError(
112
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
113
+ )
114
+ self._state = state
115
+ self.logger.debug("Initiating done")
116
+ self.initiated_event.set()
117
+ case ServiceState.RUNNING:
118
+ if self._state != ServiceState.INITIATED:
119
+ raise ServiceError(
120
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
121
+ )
122
+ self._state = state
123
+ self.logger.debug("Running")
124
+ self.started_event.set()
125
+ case ServiceState.STOPPING:
126
+ if self._state.value >= state.value:
127
+ raise ServiceError(
128
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
129
+ )
130
+ self._state = state
131
+ self.logger.debug("Stopping")
132
+ case ServiceState.STOPPED:
133
+ if (self._state != ServiceState.RUNNING) and (
134
+ self._state != ServiceState.STOPPING
135
+ ):
136
+ raise ServiceError(
137
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
138
+ )
139
+ self._state = state
140
+ self.logger.debug("Stopped")
141
+ self.stopped_event.set()
142
+ case ServiceState.SHUTDOWN_STARTED:
143
+ self._state = state
144
+ self.logger.debug("Shutting Down")
145
+ case ServiceState.SHUTDOWN:
146
+ if self._state != ServiceState.SHUTDOWN_STARTED:
147
+ raise ServiceError(
148
+ f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
149
+ )
150
+ self._state = state
151
+ self.logger.debug("Shutting Down Done")
152
+ self.shutdown_event.set()
153
+
154
+ def register(self, orchestrator, executor, name):
155
+ self.orchestrator = orchestrator
156
+ self.executor = executor
157
+ self.name = name
158
+ self._register()
159
+ self.state = ServiceState.CONSTRUCTED
160
+ self.logger = logging.getLogger(f"mellifera.service.{name}")
161
+
162
+ def _register(self):
163
+ pass
164
+
165
+ @final
166
+ def init_sync(self) -> None:
167
+ if self.state != ServiceState.CONSTRUCTED:
168
+ raise ServiceError(
169
+ f"Trying to initate a service that is in state {self.state.name}"
170
+ )
171
+ self.state = ServiceState.INITIATING
172
+ self._init_sync()
173
+ self.state = ServiceState.INITIATED
174
+
175
+ def _init_sync(self) -> None:
176
+ pass
177
+
178
+ @final
179
+ def shut_down_sync(self) -> None:
180
+ if self.state.value < ServiceState.STOPPED.value:
181
+ self.logger.error(
182
+ f"shut_down called for service {self.name}, but in state {self.state.name}"
183
+ )
184
+ if self.state.value == ServiceState.SHUTDOWN:
185
+ self.logger.error(
186
+ f"shut_down called for service {self.name}, but in state {self.state.name}"
187
+ )
188
+ return
189
+
190
+ self.state = ServiceState.SHUTDOWN_STARTED
191
+ self._shut_down_sync()
192
+ self.state = ServiceState.SHUTDOWN
193
+
194
+ def _shut_down_sync(self) -> None:
195
+ pass
@@ -0,0 +1,3 @@
1
+ from mellifera.services.trio import TrioService
2
+ from mellifera.services.nsmainthread import NSMainThreadService
3
+
@@ -0,0 +1,61 @@
1
+ from mellifera.service import Service, ServiceState, ServiceError
2
+ from typing import final
3
+
4
+
5
+ class NSMainThreadService(Service):
6
+
7
+ def stop_sync(self) -> None:
8
+ if self.state.value >= ServiceState.STOPPING.value:
9
+ return
10
+ if self.state == ServiceState.RUNNING:
11
+ self.state = ServiceState.STOPPING
12
+ self._stop_sync()
13
+ else:
14
+ self.state = ServiceState.STOPPING
15
+ self.state = ServiceState.STOPPED
16
+
17
+ def run_sync(self) -> None:
18
+ if self.state != ServiceState.INITIATED:
19
+ raise ServiceError(
20
+ f"Trying to initate a service that is in state {self.state.name}"
21
+ )
22
+ self.state = ServiceState.RUNNING
23
+
24
+ try:
25
+ self._run_sync()
26
+ self.stop_sync()
27
+ finally:
28
+ self.state = ServiceState.STOPPED
29
+ self._nursery = None
30
+
31
+ def _run_sync(self) -> None:
32
+ pass
33
+
34
+ @final
35
+ def stop(self) -> None:
36
+ if self.state.value >= ServiceState.STOPPING.value:
37
+ return
38
+ self.state = ServiceState.STOPPING
39
+ self._stop_sync()
40
+
41
+ def _stop_sync(self) -> None:
42
+ pass
43
+
44
+ @final
45
+ def shut_down_sync(self) -> None:
46
+ if self.state.value < ServiceState.STOPPED.value:
47
+ self.logger.error(
48
+ f"shut_down called for service {self.name}, but in state {self.state.name}"
49
+ )
50
+ if self.state == ServiceState.SHUTDOWN:
51
+ self.logger.error(
52
+ f"shut_down called for service {self.name}, but in state {self.state.name}"
53
+ )
54
+ return
55
+
56
+ self.state = ServiceState.SHUTDOWN_STARTED
57
+ self._shut_down_sync()
58
+ self.state = ServiceState.SHUTDOWN
59
+
60
+ def _shut_down_sync(self) -> None:
61
+ pass
@@ -0,0 +1,146 @@
1
+ import trio
2
+ from typing import final
3
+
4
+ from mellifera.service import Service, ServiceState, ServiceError
5
+
6
+
7
+ class TrioService(Service):
8
+
9
+ def __init__(self):
10
+ super().__init__()
11
+
12
+ self._nursery = None
13
+ self._trio_token = None
14
+ self._stopped = trio.Event()
15
+ self._default_is_running = trio.Event()
16
+ self._dependents = []
17
+ self._dependencies = []
18
+ self._requires = []
19
+ self.cancel_scope = trio.CancelScope()
20
+
21
+ @final
22
+ async def init(self) -> None:
23
+ if self.state != ServiceState.WILL_BE_STARTED:
24
+ raise ServiceError(
25
+ f"Trying to initate a service that is in state {self.state.name}"
26
+ )
27
+ assert self.name
28
+ assert self.orchestrator
29
+ assert self.executor
30
+ self.state = ServiceState.INITIATING
31
+
32
+ for dependency in self._dependencies:
33
+ self.orchestrator.start_service(dependency)
34
+ for dependency in self._dependencies:
35
+ await dependency.initiated
36
+ for dependency in self._requires:
37
+ await dependency.initiated
38
+ await self._init()
39
+ self.state = ServiceState.INITIATED
40
+
41
+ @final
42
+ async def run(self) -> None:
43
+ if self.state != ServiceState.INITIATED:
44
+ raise ServiceError(
45
+ f"Trying to initate a service that is in state {self.state.name}"
46
+ )
47
+ self.state = ServiceState.RUNNING
48
+
49
+ try:
50
+ with self.cancel_scope:
51
+ await self._run()
52
+ await self.stop()
53
+ finally:
54
+ self.state = ServiceState.STOPPED
55
+ self._nursery = None
56
+
57
+ @final
58
+ async def stop(self) -> None:
59
+ if self.state.value >= ServiceState.STOPPING.value:
60
+ return
61
+ if self.state == ServiceState.RUNNING:
62
+ self.state = ServiceState.STOPPING
63
+ for dependent in self._dependents:
64
+ self.logger.debug(f"Waiting for {dependent.name}")
65
+ await dependent.stopped
66
+ self.logger.debug(f"Waiting for {dependent.name} DONE")
67
+ await self._stop()
68
+ else:
69
+ self.state = ServiceState.STOPPING
70
+ self.state = ServiceState.STOPPED
71
+
72
+ def requires(self, name: str) -> Service:
73
+ if not self.orchestrator:
74
+ raise ValueError("Service is not attached to an orchestrator")
75
+ service = self.orchestrator.get_service(name)
76
+ self._requires.append(service)
77
+ return service
78
+
79
+ def depends_on(self, name: str) -> Service:
80
+ if not self.orchestrator:
81
+ raise ValueError("Service is not attached to an orchestrator")
82
+ service = self.orchestrator.get_service(name)
83
+ assert isinstance(service, TrioService)
84
+ self._dependencies.append(service)
85
+ service._dependents.append(self)
86
+ return service
87
+
88
+ def uses(self, name: str) -> Service:
89
+ if not self.orchestrator:
90
+ raise ValueError("Service is not attached to an orchestrator")
91
+ service = self.orchestrator.get_service(name)
92
+ return service
93
+
94
+
95
+
96
+ async def start(self, nursery: trio.Nursery, task_status=trio.TASK_STATUS_IGNORED) -> None:
97
+ task_status.started()
98
+ return
99
+
100
+
101
+ @final
102
+ async def cancel(self) -> None:
103
+ self.logger.debug("Cancelling")
104
+ if self._nursery:
105
+ self._nursery.cancel_scope.cancel()
106
+ self.logger.debug("Cancelling Done")
107
+
108
+ @final
109
+ async def finalize(self) -> None:
110
+ if self.state.value < ServiceState.STOPPED.value:
111
+ self.logger.error(
112
+ f"shut_down called for service {self.name}, but in state {self.state.name}"
113
+ )
114
+ if self.state == ServiceState.SHUTDOWN:
115
+ self.logger.error(
116
+ f"shut_down called for service {self.name}, but in state {self.state.name}"
117
+ )
118
+ return
119
+
120
+ self.state = ServiceState.SHUTDOWN_STARTED
121
+ await self._finalize()
122
+ self.state = ServiceState.SHUTDOWN
123
+
124
+ def _construct(self) -> None:
125
+ """Override to request other services
126
+ """
127
+ pass
128
+
129
+ async def _init(self) -> None:
130
+ """Override to customize initialization.
131
+
132
+ Aquire ressources here
133
+ """
134
+ pass
135
+
136
+ async def _run(self) -> None:
137
+ """Override to customize running behavior"""
138
+ await trio.sleep_forever()
139
+
140
+ async def _stop(self) -> None:
141
+ """Override to stop running"""
142
+ self.cancel_scope.cancel()
143
+
144
+ async def _finalize(self) -> None:
145
+ """Override to close all ressources aquired on _init"""
146
+ pass
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.3
2
+ Name: mellifera
3
+ Version: 0.2.0
4
+ Summary: Runs async services under trio and orchestrates them with a pyobjc NSMainThread service
5
+ Requires-Dist: pyobjc-core>=12.0 ; sys_platform == 'darwin'
6
+ Requires-Dist: trio>=0.32.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ Service Orchestrator
11
+
12
+ Mellifera orchestrates async services with a mainthread service
@@ -0,0 +1,13 @@
1
+ mellifera/__init__.py,sha256=P96Bx3MQntFnk4niGwkbA56aMgFtjxryd5buFQpFLNM,301
2
+ mellifera/executor.py,sha256=xCGJEIC9h8h1GgtNNaAALWs8_kzrBrVhsLZVRegX04E,1326
3
+ mellifera/executors/__init__.py,sha256=d7u4J3zwuEFzFxXYWK7gva4PkaooCfuPurAgJ76W0Jc,210
4
+ mellifera/executors/nsmainthread.py,sha256=7BAJ0KaDX6VdtsRitQhMfIKHmpu4kVPdSr0hvzRFMkM,2333
5
+ mellifera/executors/trio.py,sha256=u4OySuybuA84jK4lznBfjQHoo3WKc3sgstmj1eZpcXw,6268
6
+ mellifera/orchestrator.py,sha256=8rxGaNkwtFbusGMJPmjvYkQUxyd4vJos6-vhN4dUC1E,3132
7
+ mellifera/service.py,sha256=8aKbOEF5O6sGtuARONSJhfSzQAgvlE9BGJTvkspUyN4,6499
8
+ mellifera/services/__init__.py,sha256=otvmQMlP7eFpOvU4UV4HSidxsHslv_-iXvLi0VGvyVY,113
9
+ mellifera/services/nsmainthread.py,sha256=FmQxukzrvq_Y1xauqSm11q9jEQXQmY_d5S57nT_eth4,1804
10
+ mellifera/services/trio.py,sha256=6Kox60XKszaTx0J7CK8IKaIwwAKh0ROgKJQ3vgpfYGo,4621
11
+ mellifera-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
12
+ mellifera-0.2.0.dist-info/METADATA,sha256=GupC66Mu0SOBnnyt21v5mgWD4cnZzp1UplupH39kimI,389
13
+ mellifera-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any