jcmoptimizer 3.0.9__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. jcmoptimizer-3.2.0/PKG-INFO +138 -0
  2. jcmoptimizer-3.2.0/README.md +118 -0
  3. jcmoptimizer-3.2.0/jcmoptimizer/_progress.py +44 -0
  4. jcmoptimizer-3.2.0/jcmoptimizer/_runtime.py +273 -0
  5. jcmoptimizer-3.2.0/jcmoptimizer/async_study.py +169 -0
  6. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/benchmark.py +56 -41
  7. jcmoptimizer-3.2.0/jcmoptimizer/benchmark.pyi +111 -0
  8. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/client.py +302 -153
  9. jcmoptimizer-3.2.0/jcmoptimizer/client.pyi +190 -0
  10. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/__init__.py +0 -0
  11. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/active_learning_drivers.py +343 -211
  12. jcmoptimizer-3.2.0/jcmoptimizer/drivers/active_learning_drivers.pyi +434 -0
  13. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/driver.py +119 -62
  14. jcmoptimizer-3.2.0/jcmoptimizer/drivers/driver.pyi +192 -0
  15. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/third_party_drivers.py +6 -5
  16. jcmoptimizer-3.2.0/jcmoptimizer/drivers/third_party_drivers.pyi +17 -0
  17. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/objects.py +0 -0
  18. jcmoptimizer-3.2.0/jcmoptimizer/py.typed +0 -0
  19. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/requestor.py +125 -91
  20. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/server.py +284 -198
  21. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/study.py +238 -195
  22. jcmoptimizer-3.2.0/jcmoptimizer/study.pyi +315 -0
  23. jcmoptimizer-3.2.0/jcmoptimizer/utils/__init__.py +5 -0
  24. jcmoptimizer-3.2.0/jcmoptimizer/utils/neural_network.py +142 -0
  25. jcmoptimizer-3.2.0/jcmoptimizer/version.py +1 -0
  26. jcmoptimizer-3.2.0/jcmoptimizer.egg-info/PKG-INFO +138 -0
  27. jcmoptimizer-3.2.0/jcmoptimizer.egg-info/SOURCES.txt +41 -0
  28. jcmoptimizer-3.2.0/jcmoptimizer.egg-info/requires.txt +4 -0
  29. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/pyproject.toml +14 -3
  30. jcmoptimizer-3.2.0/tests/test_async_study.py +478 -0
  31. jcmoptimizer-3.2.0/tests/test_client_lifecycle.py +170 -0
  32. jcmoptimizer-3.2.0/tests/test_cloud_lifecycle.py +131 -0
  33. jcmoptimizer-3.2.0/tests/test_configuration.py +97 -0
  34. jcmoptimizer-3.2.0/tests/test_documentation.py +52 -0
  35. jcmoptimizer-3.2.0/tests/test_native_client.py +320 -0
  36. jcmoptimizer-3.2.0/tests/test_progress.py +59 -0
  37. jcmoptimizer-3.2.0/tests/test_requestor.py +109 -0
  38. jcmoptimizer-3.2.0/tests/test_typing.py +82 -0
  39. jcmoptimizer-3.0.9/PKG-INFO +0 -46
  40. jcmoptimizer-3.0.9/README.md +0 -28
  41. jcmoptimizer-3.0.9/jcmoptimizer/version.py +0 -1
  42. jcmoptimizer-3.0.9/jcmoptimizer.egg-info/PKG-INFO +0 -46
  43. jcmoptimizer-3.0.9/jcmoptimizer.egg-info/SOURCES.txt +0 -20
  44. jcmoptimizer-3.0.9/jcmoptimizer.egg-info/requires.txt +0 -2
  45. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/LICENSE +0 -0
  46. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/__init__.py +0 -0
  47. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer.egg-info/dependency_links.txt +0 -0
  48. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer.egg-info/top_level.txt +0 -0
  49. {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/setup.cfg +0 -0
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: jcmoptimizer
3
+ Version: 3.2.0
4
+ Summary: The JCMoptimizer interface for Python
5
+ Author-email: Philipp Schneider <philipp.schneider@jcmwave.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://optimizer.jcmwave.com
8
+ Project-URL: documentation, https://optimizer.jcmwave.com/documentation/python/latest/interface/index.html
9
+ Project-URL: releasenotes, https://optimizer.jcmwave.com/release-notes
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: httpx<1,>=0.28.1
16
+ Requires-Dist: certifi>=2026.10.19
17
+ Requires-Dist: idna>=3.20
18
+ Requires-Dist: colorama
19
+ Dynamic: license-file
20
+
21
+ # JCMoptimizer Python Client #
22
+
23
+ This package implements a python client for interacting with the JCMoptimizer server. It provides a convenient interface to set up, run, and analyze numerical studies of expensive black box functions.
24
+
25
+ ## Getting Started
26
+
27
+ ### Cloud Service
28
+ The easiest way to get started is to use the free tier of the JCMwave's cloud service for JCMoptimizer:
29
+ * Install the `jcmoptimizer` package into your python environment, e.g. by running `pip install jcmoptimizer`.
30
+ * Signup to [optimizer.jcmwave.com](https://optimizer.jcmwave.com).
31
+ * Create an [API access token](https://optimizer.jcmwave.com/cloud/tokens/list/)
32
+
33
+ ### Run JCMoptimizer Locally
34
+ For a self-hosted version, you need install JCMsuite and the JCMoptimizer extension for JCMsuite:
35
+ * Browse to the [JCMsuite download](https://installation.jcmwave.com/fb13885794fc64c42531c9656f8f6f73.php) page to install JCMsuite and to retrieve a demo or commercial license.
36
+ * Browse to the [JCMoptimizer download](https://optimizer.jcmwave.com/download) page to install JCMoptimizer.
37
+
38
+ ### Local Package Installation and Shared Configuration
39
+
40
+ The Python interface bundled with a local installation can be installed directly
41
+ into your chosen environment, with the matching version and dependencies:
42
+
43
+ ```bash
44
+ python -m pip install /path/to/JCMoptimizer/interface/python/jcmoptimizer
45
+ ```
46
+
47
+ On Windows, quote the installation directory when it contains spaces. The shared
48
+ installation can be read-only; build output is placed in temporary directories.
49
+ This installs the client, not the optimizer server. Dependency/build-tool downloads
50
+ require access to a package index or a suitable local wheelhouse.
51
+
52
+ For site-wide defaults without administrator rights, set `JCMOPTIMIZER_CONFIG` to
53
+ an existing YAML configuration file, for example:
54
+
55
+ ```bash
56
+ export JCMOPTIMIZER_CONFIG=/shared/software/config/jcmoptimizer.yml
57
+ ```
58
+
59
+ ```powershell
60
+ $env:JCMOPTIMIZER_CONFIG = 'Z:\software\config\jcmoptimizer.yml'
61
+ ```
62
+
63
+ Its settings are overridden by `~/.jcmoptimizer.conf.yml`, then by
64
+ `.jcmoptimizer.conf.yml` in the current working directory, and finally by explicit
65
+ Python arguments. An unset/empty variable adds no file; a selected file that is
66
+ missing, unreadable or malformed raises an error. Keep personal tokens in the home
67
+ file. A shared local setup can contain:
68
+
69
+ ```yaml
70
+ server_location: local
71
+ jcmoptimizer_dir: /shared/software/JCMoptimizer
72
+ license_dir: /shared/software/JCMsuite/license
73
+ ```
74
+
75
+ ### First Example
76
+ A basic usage example of the python package is given in the tutorial on a [Bayesian Optimization](https://optimizer.jcmwave.com/documentation/python/latest/tutorials/vanilla_bayesian_optimization.html).
77
+
78
+ ## Documentation
79
+
80
+ ### Synchronous and asynchronous clients
81
+
82
+ Remote methods use one implementation and one method reference. Calls block by
83
+ default; `Client(asynchronous=True)` returns coroutines for remote operations.
84
+ Its constructor does no network I/O. Context entry or the first awaited operation
85
+ connects or starts the configured server without an explicit `Server` instance:
86
+
87
+ ```python
88
+ async with Client(asynchronous=True) as client:
89
+ study = await client.create_study(design_space=design_space, open_browser=False)
90
+ await study.configure(max_iter=20, num_parallel=2)
91
+ study.set_evaluator(evaluate)
92
+ await study.run()
93
+ minimum = await study.driver.get_min_objective()
94
+ best = await study.driver.get_best_sample()
95
+ ```
96
+
97
+ The mode propagates to studies, benchmarks and drivers. Local operations such as
98
+ `set_evaluator`, `new_observation`, and observation editing remain regular calls.
99
+ In blocking code, `with Client() as client:` and `driver.min_objective` still work.
100
+ Network-backed properties require their `get_*()` method in async mode. Generated
101
+ typing declarations select the result or coroutine from the client mode; regenerate
102
+ them with `python tools/generate_typing.py` after changing coroutine signatures.
103
+
104
+ Async clients and their handles belong to one event loop. Use an async context or
105
+ `await client.close()` for cleanup; async destructors do not perform I/O. Closing
106
+ releases remote handles and HTTP connections, cancels active operations and stops
107
+ an owned nonpersistent server. Supplied hosts and reused cloud servers remain
108
+ running. `persistent=True` also preserves a newly started server. Explicit
109
+ `await client.shutdown_server()` can stop a borrowed server when intended.
110
+ Clients, studies and benchmarks all use `close()` in synchronous mode and
111
+ `await close()` in asynchronous mode.
112
+ Standalone synchronous `Server` remains available; no async server class is needed.
113
+
114
+ HTTP requests use pooled native HTTPX coroutines with no execution/read timeout.
115
+ Synchronous clients use a single background I/O loop, rather than a worker per
116
+ request. Async evaluator functions run on the caller loop; synchronous evaluators
117
+ run in a dedicated thread pool and must be thread-safe. Server `num_parallel`
118
+ controls evaluation concurrency. Both modes use `study.run()`; `run_async()` was
119
+ removed before the 3.2.0 release.
120
+
121
+ Evaluator failures are logged and tolerated until `study.max_num_failed`
122
+ consecutive failures (default: 3), at which point the last error propagates.
123
+ Successful observation submission resets the failure count. Request and upload
124
+ errors propagate immediately.
125
+
126
+ Ctrl-C in blocking `run()`, or cancellation of async `run()`, cancels evaluations,
127
+ clears owned suggestions and stops the study. In-flight mutations settle before
128
+ cleanup, so an unresponsive server can delay cancellation. Python worker threads
129
+ cannot be forcibly cancelled and may delay interpreter exit. Observations and
130
+ continuation settings are preserved. Closing the client then applies server
131
+ ownership rules. Benchmarks use the same mode and require studies from that client.
132
+
133
+ For a comprehensive overview of all available classes, methods, and their parameters, please refer to the official JCMoptimizer Python documentation:
134
+
135
+ * [JCMoptimizer Python Interface Documentation](https://optimizer.jcmwave.com/documentation/python/latest/interface/index.html)
136
+ * [JCMoptimizer Python Tutorials](https://optimizer.jcmwave.com/documentation/python/latest/tutorials/index.html)
137
+
138
+ These resources provide detailed explanations and examples for advanced usage, including different drivers.
@@ -0,0 +1,118 @@
1
+ # JCMoptimizer Python Client #
2
+
3
+ This package implements a python client for interacting with the JCMoptimizer server. It provides a convenient interface to set up, run, and analyze numerical studies of expensive black box functions.
4
+
5
+ ## Getting Started
6
+
7
+ ### Cloud Service
8
+ The easiest way to get started is to use the free tier of the JCMwave's cloud service for JCMoptimizer:
9
+ * Install the `jcmoptimizer` package into your python environment, e.g. by running `pip install jcmoptimizer`.
10
+ * Signup to [optimizer.jcmwave.com](https://optimizer.jcmwave.com).
11
+ * Create an [API access token](https://optimizer.jcmwave.com/cloud/tokens/list/)
12
+
13
+ ### Run JCMoptimizer Locally
14
+ For a self-hosted version, you need install JCMsuite and the JCMoptimizer extension for JCMsuite:
15
+ * Browse to the [JCMsuite download](https://installation.jcmwave.com/fb13885794fc64c42531c9656f8f6f73.php) page to install JCMsuite and to retrieve a demo or commercial license.
16
+ * Browse to the [JCMoptimizer download](https://optimizer.jcmwave.com/download) page to install JCMoptimizer.
17
+
18
+ ### Local Package Installation and Shared Configuration
19
+
20
+ The Python interface bundled with a local installation can be installed directly
21
+ into your chosen environment, with the matching version and dependencies:
22
+
23
+ ```bash
24
+ python -m pip install /path/to/JCMoptimizer/interface/python/jcmoptimizer
25
+ ```
26
+
27
+ On Windows, quote the installation directory when it contains spaces. The shared
28
+ installation can be read-only; build output is placed in temporary directories.
29
+ This installs the client, not the optimizer server. Dependency/build-tool downloads
30
+ require access to a package index or a suitable local wheelhouse.
31
+
32
+ For site-wide defaults without administrator rights, set `JCMOPTIMIZER_CONFIG` to
33
+ an existing YAML configuration file, for example:
34
+
35
+ ```bash
36
+ export JCMOPTIMIZER_CONFIG=/shared/software/config/jcmoptimizer.yml
37
+ ```
38
+
39
+ ```powershell
40
+ $env:JCMOPTIMIZER_CONFIG = 'Z:\software\config\jcmoptimizer.yml'
41
+ ```
42
+
43
+ Its settings are overridden by `~/.jcmoptimizer.conf.yml`, then by
44
+ `.jcmoptimizer.conf.yml` in the current working directory, and finally by explicit
45
+ Python arguments. An unset/empty variable adds no file; a selected file that is
46
+ missing, unreadable or malformed raises an error. Keep personal tokens in the home
47
+ file. A shared local setup can contain:
48
+
49
+ ```yaml
50
+ server_location: local
51
+ jcmoptimizer_dir: /shared/software/JCMoptimizer
52
+ license_dir: /shared/software/JCMsuite/license
53
+ ```
54
+
55
+ ### First Example
56
+ A basic usage example of the python package is given in the tutorial on a [Bayesian Optimization](https://optimizer.jcmwave.com/documentation/python/latest/tutorials/vanilla_bayesian_optimization.html).
57
+
58
+ ## Documentation
59
+
60
+ ### Synchronous and asynchronous clients
61
+
62
+ Remote methods use one implementation and one method reference. Calls block by
63
+ default; `Client(asynchronous=True)` returns coroutines for remote operations.
64
+ Its constructor does no network I/O. Context entry or the first awaited operation
65
+ connects or starts the configured server without an explicit `Server` instance:
66
+
67
+ ```python
68
+ async with Client(asynchronous=True) as client:
69
+ study = await client.create_study(design_space=design_space, open_browser=False)
70
+ await study.configure(max_iter=20, num_parallel=2)
71
+ study.set_evaluator(evaluate)
72
+ await study.run()
73
+ minimum = await study.driver.get_min_objective()
74
+ best = await study.driver.get_best_sample()
75
+ ```
76
+
77
+ The mode propagates to studies, benchmarks and drivers. Local operations such as
78
+ `set_evaluator`, `new_observation`, and observation editing remain regular calls.
79
+ In blocking code, `with Client() as client:` and `driver.min_objective` still work.
80
+ Network-backed properties require their `get_*()` method in async mode. Generated
81
+ typing declarations select the result or coroutine from the client mode; regenerate
82
+ them with `python tools/generate_typing.py` after changing coroutine signatures.
83
+
84
+ Async clients and their handles belong to one event loop. Use an async context or
85
+ `await client.close()` for cleanup; async destructors do not perform I/O. Closing
86
+ releases remote handles and HTTP connections, cancels active operations and stops
87
+ an owned nonpersistent server. Supplied hosts and reused cloud servers remain
88
+ running. `persistent=True` also preserves a newly started server. Explicit
89
+ `await client.shutdown_server()` can stop a borrowed server when intended.
90
+ Clients, studies and benchmarks all use `close()` in synchronous mode and
91
+ `await close()` in asynchronous mode.
92
+ Standalone synchronous `Server` remains available; no async server class is needed.
93
+
94
+ HTTP requests use pooled native HTTPX coroutines with no execution/read timeout.
95
+ Synchronous clients use a single background I/O loop, rather than a worker per
96
+ request. Async evaluator functions run on the caller loop; synchronous evaluators
97
+ run in a dedicated thread pool and must be thread-safe. Server `num_parallel`
98
+ controls evaluation concurrency. Both modes use `study.run()`; `run_async()` was
99
+ removed before the 3.2.0 release.
100
+
101
+ Evaluator failures are logged and tolerated until `study.max_num_failed`
102
+ consecutive failures (default: 3), at which point the last error propagates.
103
+ Successful observation submission resets the failure count. Request and upload
104
+ errors propagate immediately.
105
+
106
+ Ctrl-C in blocking `run()`, or cancellation of async `run()`, cancels evaluations,
107
+ clears owned suggestions and stops the study. In-flight mutations settle before
108
+ cleanup, so an unresponsive server can delay cancellation. Python worker threads
109
+ cannot be forcibly cancelled and may delay interpreter exit. Observations and
110
+ continuation settings are preserved. Closing the client then applies server
111
+ ownership rules. Benchmarks use the same mode and require studies from that client.
112
+
113
+ For a comprehensive overview of all available classes, methods, and their parameters, please refer to the official JCMoptimizer Python documentation:
114
+
115
+ * [JCMoptimizer Python Interface Documentation](https://optimizer.jcmwave.com/documentation/python/latest/interface/index.html)
116
+ * [JCMoptimizer Python Tutorials](https://optimizer.jcmwave.com/documentation/python/latest/tutorials/index.html)
117
+
118
+ These resources provide detailed explanations and examples for advanced usage, including different drivers.
@@ -0,0 +1,44 @@
1
+ """Terminal progress driven by the client's existing event loop."""
2
+
3
+ import asyncio
4
+ import itertools
5
+ import sys
6
+ from typing import Any, Optional
7
+
8
+ from ._runtime import drain
9
+ from .requestor import inform
10
+
11
+
12
+ class WorkingAnimation:
13
+ def __init__(self, message: str, enabled: bool = True) -> None:
14
+ self.message = message
15
+ self.enabled = enabled
16
+ self.stream = sys.stdout
17
+ self.animated = enabled and self.stream.isatty()
18
+ self._width = 0
19
+ self._task: Optional[asyncio.Task[None]] = None
20
+
21
+ def update(self, message: str) -> None:
22
+ self.message = message
23
+ if self.enabled and not self.animated:
24
+ inform(message)
25
+
26
+ async def _animate(self) -> None:
27
+ for frame in itertools.cycle("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"):
28
+ text = f"{frame} {self.message}"
29
+ self._width = max(self._width, len(text))
30
+ self.stream.write("\r" + text.ljust(self._width))
31
+ self.stream.flush()
32
+ await asyncio.sleep(0.15)
33
+
34
+ async def __aenter__(self) -> "WorkingAnimation":
35
+ if self.animated:
36
+ self._task = asyncio.create_task(self._animate())
37
+ return self
38
+
39
+ async def __aexit__(self, *exc: Any) -> None:
40
+ if self._task is not None:
41
+ self._task.cancel()
42
+ await drain(self._task)
43
+ self.stream.write("\r" + " " * self._width + "\r")
44
+ self.stream.flush()
@@ -0,0 +1,273 @@
1
+ """One mode boundary for the native async interface and synchronous callers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ import importlib
8
+ import inspect
9
+ import threading
10
+ import traceback
11
+ from collections.abc import Awaitable, Callable
12
+ from concurrent.futures import TimeoutError as FutureTimeout
13
+ from typing import Any, TypeVar
14
+
15
+ Mode = TypeVar("Mode", bound=bool)
16
+ T = TypeVar("T")
17
+
18
+
19
+ _exit_callbacks: dict[Callable[[], None], None] = {}
20
+ _exit_lock = threading.Lock()
21
+ _exit_registered = False
22
+
23
+
24
+ def register_exit_cleanup(callback: Callable[[], None]) -> None:
25
+ """Close blocking clients while asyncio's DNS executor is still usable."""
26
+ global _exit_registered
27
+ with _exit_lock:
28
+ if not _exit_registered:
29
+ # ThreadPoolExecutor uses threading's pre-join exit hooks. Import
30
+ # its hook first so ours runs before it, including with check=False.
31
+ importlib.import_module("concurrent.futures.thread")
32
+ # Available since Python 3.9, but omitted from threading's stubs.
33
+ threading._register_atexit(_close_at_exit) # type: ignore[attr-defined]
34
+ _exit_registered = True
35
+ _exit_callbacks[callback] = None
36
+
37
+
38
+ def unregister_exit_cleanup(callback: Callable[[], None]) -> None:
39
+ with _exit_lock:
40
+ _exit_callbacks.pop(callback, None)
41
+
42
+
43
+ def _close_at_exit() -> None:
44
+ with _exit_lock:
45
+ callbacks = list(_exit_callbacks)
46
+ for callback in reversed(callbacks):
47
+ try:
48
+ callback()
49
+ except Exception:
50
+ # As with atexit, report unexpected failures and continue closing
51
+ # the remaining clients. Explicit close still propagates errors.
52
+ traceback.print_exc()
53
+
54
+
55
+ async def settle(awaitable: Awaitable[T]) -> T:
56
+ """Settle a remote mutation before propagating cancellation to its owner."""
57
+ task = asyncio.ensure_future(awaitable)
58
+ try:
59
+ return await asyncio.shield(task)
60
+ except asyncio.CancelledError:
61
+ await drain(task)
62
+ raise
63
+
64
+
65
+ async def drain(awaitable: Awaitable[T]) -> T | BaseException:
66
+ """Finish cleanup/recover a mutation even when cancellation is repeated."""
67
+ task = asyncio.ensure_future(awaitable)
68
+ while not task.done():
69
+ try:
70
+ await asyncio.shield(task)
71
+ except asyncio.CancelledError:
72
+ pass
73
+ except Exception:
74
+ break
75
+ try:
76
+ return task.result()
77
+ except BaseException as error:
78
+ return error
79
+
80
+
81
+ class Runtime:
82
+ def __init__(self, asynchronous: bool = False) -> None:
83
+ self.asynchronous = asynchronous
84
+ self.closed = False
85
+ self.closing = False
86
+ self.sessions: list[Any] = []
87
+ self.active: set[asyncio.Task[Any]] = set()
88
+ self.loop: asyncio.AbstractEventLoop | None = None
89
+ self.thread: threading.Thread | None = None
90
+ self._start_lock = threading.Lock()
91
+
92
+ def bind(self) -> None:
93
+ loop = asyncio.get_running_loop()
94
+ if self.loop is None:
95
+ self.loop = loop
96
+ elif self.loop is not loop:
97
+ raise RuntimeError(
98
+ "Use an asynchronous client only on its owning event loop."
99
+ )
100
+
101
+ def _start(self) -> None:
102
+ with self._start_lock:
103
+ if self.thread is not None:
104
+ return
105
+ ready = threading.Event()
106
+
107
+ def serve() -> None:
108
+ self.loop = asyncio.new_event_loop()
109
+ asyncio.set_event_loop(self.loop)
110
+ ready.set()
111
+ try:
112
+ self.loop.run_forever()
113
+ finally:
114
+ self.loop.run_until_complete(self.loop.shutdown_asyncgens())
115
+ self.loop.run_until_complete(self.loop.shutdown_default_executor())
116
+ self.loop.close()
117
+
118
+ self.thread = threading.Thread(
119
+ target=serve, name="jcmoptimizer-io", daemon=True
120
+ )
121
+ self.thread.start()
122
+ ready.wait()
123
+
124
+ def blocking(self, factory: Callable[[], Awaitable[T]]) -> T:
125
+ self._start()
126
+ if threading.current_thread() is self.thread:
127
+ raise RuntimeError(
128
+ "A blocking client call cannot run on its I/O loop; use Client(asynchronous=True)."
129
+ )
130
+ assert self.loop is not None
131
+ finished = threading.Event()
132
+
133
+ async def invoke() -> T:
134
+ try:
135
+ return await factory()
136
+ finally:
137
+ finished.set()
138
+
139
+ future = asyncio.run_coroutine_threadsafe(invoke(), self.loop)
140
+ try:
141
+ while True:
142
+ try:
143
+ return future.result(timeout=0.1)
144
+ except FutureTimeout:
145
+ if future.done():
146
+ return future.result()
147
+ except KeyboardInterrupt:
148
+ future.cancel()
149
+ # The future becomes cancelled before coroutine cleanup has finished.
150
+ while not finished.wait(0.1):
151
+ pass
152
+ raise
153
+
154
+ async def execute(
155
+ self,
156
+ owner: Any,
157
+ function: Callable[..., Awaitable[T]],
158
+ args: tuple[Any, ...],
159
+ kwargs: dict[str, Any],
160
+ ) -> T:
161
+ self.bind()
162
+ name = function.__name__
163
+ if self.closed:
164
+ if name == "_async_close":
165
+ return None # type: ignore[return-value]
166
+ raise RuntimeError("The JCMoptimizer client is closed.")
167
+ if name == "_async_close":
168
+ return await function(owner, *args, **kwargs)
169
+ if self.closing:
170
+ raise RuntimeError("The JCMoptimizer client is closing.")
171
+
172
+ async def invoke() -> T:
173
+ connect = getattr(owner, "_async_connect", None)
174
+ if connect is not None and name not in {"_async_connect", "_async_close"}:
175
+ await connect()
176
+ if name in {"_async_run", "_async_close", "_async_connect"} or getattr(
177
+ function, "__interruptible__", False
178
+ ):
179
+ return await function(owner, *args, **kwargs)
180
+ request = asyncio.ensure_future(function(owner, *args, **kwargs))
181
+ try:
182
+ return await asyncio.shield(request)
183
+ except asyncio.CancelledError:
184
+ result = await drain(request)
185
+ cleanup = getattr(owner, "_async_cancel_result", None)
186
+ if cleanup is not None and not isinstance(result, BaseException):
187
+ await settle(cleanup(name, result))
188
+ raise
189
+
190
+ # Track the operation, not its caller: context exit may itself join close.
191
+ task = asyncio.create_task(invoke())
192
+ self.active.add(task)
193
+ try:
194
+ return await task
195
+ finally:
196
+ self.active.discard(task)
197
+
198
+ async def cancel_pending(self) -> None:
199
+ current = asyncio.current_task()
200
+ pending = [task for task in self.active if task is not current]
201
+ for task in pending:
202
+ if not getattr(task, "cancelling", lambda: 0)():
203
+ task.cancel()
204
+ await asyncio.gather(*pending, return_exceptions=True)
205
+
206
+ async def close_sessions(self) -> None:
207
+ for session in self.sessions:
208
+ await session.aclose()
209
+ self.sessions.clear()
210
+ self.closed = True
211
+
212
+ def stop(self) -> None:
213
+ if (
214
+ self.thread is not None
215
+ and self.loop is not None
216
+ and not self.loop.is_closed()
217
+ ):
218
+ self.loop.call_soon_threadsafe(self.loop.stop)
219
+ self.thread.join()
220
+
221
+
222
+ def operation(
223
+ function: Callable[..., Awaitable[T]],
224
+ *,
225
+ name: str | None = None,
226
+ interruptible: bool = False,
227
+ ) -> Callable[..., Any]:
228
+ """Expose one coroutine through the client's explicit execution mode."""
229
+ public = name or function.__name__.removeprefix("_async_")
230
+ if interruptible:
231
+ setattr(function, "__interruptible__", True)
232
+
233
+ @functools.wraps(function)
234
+ def invoke(self: Any, *args: Any, **kwargs: Any) -> Any:
235
+ runtime: Runtime = self._runtime
236
+ if runtime.closed:
237
+ if function.__name__ == "_async_close":
238
+
239
+ async def closed() -> None:
240
+ return None
241
+
242
+ return closed() if runtime.asynchronous else None
243
+ raise RuntimeError("The JCMoptimizer client is closed.")
244
+ factory = lambda: runtime.execute(self, function, args, kwargs)
245
+ if runtime.asynchronous:
246
+ return factory()
247
+ try:
248
+ return runtime.blocking(factory)
249
+ except KeyboardInterrupt:
250
+ if public != "run":
251
+ raise
252
+ from .requestor import warn
253
+
254
+ warn("Study stopped.")
255
+ return None
256
+ finally:
257
+ if runtime.closed:
258
+ runtime.stop()
259
+
260
+ invoke.__name__ = public
261
+ invoke.__qualname__ = function.__qualname__.rsplit(".", 1)[0] + "." + public
262
+ invoke.__doc__ = inspect.cleandoc(function.__doc__ or "")
263
+ if not function.__qualname__.startswith("Server."):
264
+ invoke.__doc__ += "\n\nWith ``Client(asynchronous=True)``, await this method."
265
+ return invoke
266
+
267
+
268
+ def blocking_property(owner: Any, getter: str) -> Any:
269
+ if owner._runtime.asynchronous:
270
+ raise RuntimeError(
271
+ f"This property performs I/O. Use await {getter}() with an asynchronous client."
272
+ )
273
+ return getattr(owner, getter)()