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.
- jcmoptimizer-3.2.0/PKG-INFO +138 -0
- jcmoptimizer-3.2.0/README.md +118 -0
- jcmoptimizer-3.2.0/jcmoptimizer/_progress.py +44 -0
- jcmoptimizer-3.2.0/jcmoptimizer/_runtime.py +273 -0
- jcmoptimizer-3.2.0/jcmoptimizer/async_study.py +169 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/benchmark.py +56 -41
- jcmoptimizer-3.2.0/jcmoptimizer/benchmark.pyi +111 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/client.py +302 -153
- jcmoptimizer-3.2.0/jcmoptimizer/client.pyi +190 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/__init__.py +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/active_learning_drivers.py +343 -211
- jcmoptimizer-3.2.0/jcmoptimizer/drivers/active_learning_drivers.pyi +434 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/driver.py +119 -62
- jcmoptimizer-3.2.0/jcmoptimizer/drivers/driver.pyi +192 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/drivers/third_party_drivers.py +6 -5
- jcmoptimizer-3.2.0/jcmoptimizer/drivers/third_party_drivers.pyi +17 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/objects.py +0 -0
- jcmoptimizer-3.2.0/jcmoptimizer/py.typed +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/requestor.py +125 -91
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/server.py +284 -198
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/study.py +238 -195
- jcmoptimizer-3.2.0/jcmoptimizer/study.pyi +315 -0
- jcmoptimizer-3.2.0/jcmoptimizer/utils/__init__.py +5 -0
- jcmoptimizer-3.2.0/jcmoptimizer/utils/neural_network.py +142 -0
- jcmoptimizer-3.2.0/jcmoptimizer/version.py +1 -0
- jcmoptimizer-3.2.0/jcmoptimizer.egg-info/PKG-INFO +138 -0
- jcmoptimizer-3.2.0/jcmoptimizer.egg-info/SOURCES.txt +41 -0
- jcmoptimizer-3.2.0/jcmoptimizer.egg-info/requires.txt +4 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/pyproject.toml +14 -3
- jcmoptimizer-3.2.0/tests/test_async_study.py +478 -0
- jcmoptimizer-3.2.0/tests/test_client_lifecycle.py +170 -0
- jcmoptimizer-3.2.0/tests/test_cloud_lifecycle.py +131 -0
- jcmoptimizer-3.2.0/tests/test_configuration.py +97 -0
- jcmoptimizer-3.2.0/tests/test_documentation.py +52 -0
- jcmoptimizer-3.2.0/tests/test_native_client.py +320 -0
- jcmoptimizer-3.2.0/tests/test_progress.py +59 -0
- jcmoptimizer-3.2.0/tests/test_requestor.py +109 -0
- jcmoptimizer-3.2.0/tests/test_typing.py +82 -0
- jcmoptimizer-3.0.9/PKG-INFO +0 -46
- jcmoptimizer-3.0.9/README.md +0 -28
- jcmoptimizer-3.0.9/jcmoptimizer/version.py +0 -1
- jcmoptimizer-3.0.9/jcmoptimizer.egg-info/PKG-INFO +0 -46
- jcmoptimizer-3.0.9/jcmoptimizer.egg-info/SOURCES.txt +0 -20
- jcmoptimizer-3.0.9/jcmoptimizer.egg-info/requires.txt +0 -2
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/LICENSE +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer/__init__.py +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer.egg-info/dependency_links.txt +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.2.0}/jcmoptimizer.egg-info/top_level.txt +0 -0
- {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)()
|