Skip to content

Commit 540d038

Browse files
authored
Merge pull request #143 from knopki/fix-nested-async-loop
Fix nested async loop
2 parents 7cbe0ee + e0e4a2e commit 540d038

File tree

18 files changed

+154
-101
lines changed

18 files changed

+154
-101
lines changed

.github/workflows/linter.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
python-version: ["3.8"]
15+
python-version: ["3.9"]
1616

1717
steps:
1818
- name: Checkout repository
19-
uses: actions/checkout@v3
19+
uses: actions/checkout@v4
2020
if: ${{ !env.ACT }} # skip during local actions testing
2121
with:
2222
fetch-depth: 0
2323

2424
- name: Setup Python
25-
uses: actions/setup-python@v4
25+
uses: actions/setup-python@v5
2626
with:
2727
python-version: ${{ matrix.python-version }}
2828
cache: pip
@@ -51,4 +51,4 @@ jobs:
5151
run: pylint -E yascheduler
5252

5353
- name: pyupgrade
54-
run: pyupgrade --py38-plus --keep-percent-format
54+
run: pyupgrade --py39-plus --keep-percent-format

.github/workflows/pr.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ jobs:
1616

1717
steps:
1818
- name: Checkout repository
19-
uses: actions/checkout@v3
19+
uses: actions/checkout@v4
2020
if: ${{ !env.ACT }} # skip during local actions testing
2121

2222
- name: Setup Python
23-
uses: actions/setup-python@v4
23+
uses: actions/setup-python@v5
2424
with:
25-
python-version: 3.11
25+
python-version: 3.13
2626
cache: pip
2727
cache-dependency-path: pyproject.toml
2828

.github/workflows/push.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ jobs:
1818

1919
steps:
2020
- name: Checkout repository
21-
uses: actions/checkout@v3
21+
uses: actions/checkout@v4
2222
if: ${{ !env.ACT }} # skip during local actions testing
2323
with:
2424
fetch-depth: 0
2525

2626
- name: Setup Python
27-
uses: actions/setup-python@v4
27+
uses: actions/setup-python@v5
2828
with:
29-
python-version: 3.11
29+
python-version: 3.13
3030
cache: pip
3131
cache-dependency-path: pyproject.toml
3232

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ jobs:
2121

2222
steps:
2323
- name: Checkout repository
24-
uses: actions/checkout@v3
24+
uses: actions/checkout@v4
2525
if: ${{ !env.ACT }} # skip during local actions testing
2626
with:
2727
fetch-depth: 0
2828

2929
- name: Setup Python
30-
uses: actions/setup-python@v4
30+
uses: actions/setup-python@v5
3131
with:
32-
python-version: 3.11
32+
python-version: 3.13
3333
cache: pip
3434
cache-dependency-path: pyproject.toml
3535

examples/submit_any_engine_input.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
parser = argparse.ArgumentParser()
99
parser.add_argument("-f", dest="file", action="store", type=str, required=True)
1010
parser.add_argument("-e", dest="engine", action="store", type=str, required=True)
11-
parser.add_argument("-l", dest="localrepo", action="store", type=bool, required=False, default=False)
11+
parser.add_argument(
12+
"-l", dest="localrepo", action="store", type=bool, required=False, default=False
13+
)
1214
args = parser.parse_args()
1315

1416
input_data = {}

examples/submit_pcrystal_input.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
FOLDER = work_folder
2121
print("**To save calc in an input folder**")
2222

23-
f34_name = os.path.basename(target).split('.')[0] + '.f34' # e.g. archive with *.f34
23+
f34_name = os.path.basename(target).split(".")[0] + ".f34" # e.g. archive with *.f34
2424

2525
if os.path.exists(os.path.join(work_folder, "fort.34")):
2626
assert "EXTERNAL" in SETUP_INPUT

examples/submit_topas_input.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838

3939

4040
yac = Yascheduler()
41-
result = yac.queue_submit_task(LABEL, {"calc.inp": PATTERN_REQUEST, "structure.inc": ""}, "topas")
41+
result = yac.queue_submit_task(
42+
LABEL, {"calc.inp": PATTERN_REQUEST, "structure.inc": ""}, "topas"
43+
)
4244
print(LABEL)
4345
print(result)

pyproject.toml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ classifiers = [
2424
"Topic :: Software Development :: Libraries :: Python Modules",
2525
"License :: OSI Approved :: MIT License",
2626
"Programming Language :: Python :: 3",
27-
"Programming Language :: Python :: 3.7",
28-
"Programming Language :: Python :: 3.8",
2927
"Programming Language :: Python :: 3.9",
3028
"Programming Language :: Python :: 3.10",
3129
"Programming Language :: Python :: 3.11",
32-
"Framework :: AiiDA"
30+
"Programming Language :: Python :: 3.12",
31+
"Programming Language :: Python :: 3.13",
32+
"Framework :: AiiDA",
3333
]
34-
requires-python = ">=3.8"
34+
requires-python = ">=3.9"
3535
dependencies = [
3636
"aiohttp~=3.8",
3737
"asyncssh~=2.11",
@@ -46,7 +46,6 @@ dependencies = [
4646
"python-daemon~=2.3",
4747
"typing-extensions >= 4.2.0; python_version < '3.11'",
4848
"upcloud_api~=2.0",
49-
"importlib_metadata; python_version < '3.8'",
5049
]
5150

5251
[project.optional-dependencies]
@@ -96,7 +95,7 @@ remove-duplicate-keys = true
9695
remove-unused-variables = true
9796

9897
[tool.black]
99-
target-version = ['py38', 'py39', 'py310', 'py311']
98+
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
10099

101100

102101
[tool.commitizen]
@@ -111,7 +110,7 @@ changelog_incremental = true
111110

112111
[tool.isort]
113112
profile = "black"
114-
py_version = 38
113+
py_version = 39
115114

116115
[tool.pylint.MASTER]
117116
load-plugins=[
@@ -120,7 +119,7 @@ load-plugins=[
120119

121120
[tool.pylint.main]
122121
jobs = 0
123-
py-version = "3.8"
122+
py-version = "3.9"
124123
recursive = true
125124
suggestion-mode = true
126125

yascheduler/__init__.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import sys
1+
import importlib.metadata
22

33
from .client import Yascheduler
44
from .variables import CONFIG_FILE, LOG_FILE, PID_FILE
55

6-
if sys.version_info < (3, 8):
7-
import importlib_metadata
8-
else:
9-
import importlib.metadata as importlib_metadata
10-
11-
__version__ = importlib_metadata.version("yascheduler")
6+
__version__ = importlib.metadata.version("yascheduler")
127
__all__ = [
138
"CONFIG_FILE",
149
"LOG_FILE",

yascheduler/client.py

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,61 @@
22

33
import asyncio
44
import logging
5+
import sys
6+
from concurrent.futures import ThreadPoolExecutor
7+
from functools import wraps
58
from pathlib import PurePath
6-
from typing import Any, Mapping, Optional, Sequence, Union
9+
from typing import (
10+
Any,
11+
Callable,
12+
Coroutine,
13+
Mapping,
14+
Optional,
15+
Sequence,
16+
TypeVar,
17+
Union,
18+
)
719

820
from attrs import asdict
921

1022
from .config import Config
11-
from .db import DB, TaskModel, TaskStatus
23+
from .db import DB, TaskStatus
1224
from .scheduler import Scheduler
1325
from .variables import CONFIG_FILE
1426

27+
if sys.version_info < (3, 10):
28+
from typing_extensions import ParamSpec
29+
else:
30+
from typing import ParamSpec
31+
32+
ReturnT_co = TypeVar("ReturnT_co", covariant=True)
33+
ParamT = ParamSpec("ParamT")
34+
35+
36+
def to_sync(
37+
func: Callable[ParamT, Coroutine[Any, Any, ReturnT_co]],
38+
) -> Callable[ParamT, ReturnT_co]:
39+
"""
40+
Wraps async function and run it sync in thread.
41+
"""
42+
43+
@wraps(func)
44+
def outer(*args: ParamT.args, **kwargs: ParamT.kwargs):
45+
"""
46+
Execute the async method synchronously in sync and async runtime.
47+
"""
48+
coro = func(*args, **kwargs)
49+
try:
50+
asyncio.get_running_loop() # Triggers RuntimeError if no running event loop
51+
52+
# Create a separate thread so we can block before returning
53+
with ThreadPoolExecutor(1) as pool:
54+
return pool.submit(lambda: asyncio.run(coro)).result()
55+
except RuntimeError:
56+
return asyncio.run(coro)
57+
58+
return outer
59+
1560

1661
class Yascheduler:
1762
"""Yascheduler client"""
@@ -31,30 +76,36 @@ def __init__(
3176
self.config = Config.from_config_parser(config_path)
3277
self._logger = logger
3378

34-
def queue_submit_task(
79+
async def queue_submit_task_async(
3580
self,
3681
label: str,
3782
metadata: Mapping[str, Any],
3883
engine_name: str,
3984
webhook_onsubmit=False,
4085
) -> int:
4186
"""Submit new task"""
42-
43-
async def async_fn() -> TaskModel:
44-
yac = await Scheduler.create(config=self.config, log=self._logger)
45-
task = await yac.create_new_task(
46-
label=label,
47-
metadata=metadata,
48-
engine_name=engine_name,
49-
webhook_onsubmit=webhook_onsubmit,
50-
)
51-
await yac.stop()
52-
return task
53-
54-
task = asyncio.run(async_fn())
87+
yac = await Scheduler.create(config=self.config, log=self._logger)
88+
task = await yac.create_new_task(
89+
label=label,
90+
metadata=metadata,
91+
engine_name=engine_name,
92+
webhook_onsubmit=webhook_onsubmit,
93+
)
94+
await yac.stop()
5595
return task.task_id
5696

57-
def queue_get_tasks(
97+
def queue_submit_task(
98+
self,
99+
label: str,
100+
metadata: Mapping[str, Any],
101+
engine_name: str,
102+
webhook_onsubmit=False,
103+
) -> int:
104+
"""Submit new task"""
105+
fn = to_sync(self.queue_submit_task_async)
106+
return fn(label, metadata, engine_name, webhook_onsubmit)
107+
108+
async def queue_get_tasks_async(
58109
self,
59110
jobs: Optional[Sequence[int]] = None,
60111
status: Optional[Sequence[int]] = None,
@@ -64,24 +115,28 @@ def queue_get_tasks(
64115
raise ValueError("jobs can be selected only by status or by task ids")
65116
# raise ValueError if unknown task status
66117
status = [TaskStatus(x) for x in status] if status else None
67-
68-
async def fn_get_by_statuses(statuses: Sequence[TaskStatus]):
69-
db = await DB.create(self.config.db)
70-
return await db.get_tasks_by_status(statuses)
71-
72-
async def fn_get_by_ids(ids: Sequence[int]):
73-
db = await DB.create(self.config.db)
74-
return await db.get_tasks_by_jobs(ids)
75-
118+
db = await DB.create(self.config.db)
76119
if status:
77-
tasks = asyncio.run(fn_get_by_statuses(status))
120+
tasks = await db.get_tasks_by_status(status)
78121
elif jobs:
79-
tasks = asyncio.run(fn_get_by_ids(jobs))
122+
tasks = await db.get_tasks_by_jobs(jobs)
80123
else:
81124
return []
82-
83125
return [asdict(t) for t in tasks]
84126

127+
def queue_get_tasks(
128+
self,
129+
jobs: Optional[Sequence[int]] = None,
130+
status: Optional[Sequence[int]] = None,
131+
) -> Sequence[Mapping[str, Any]]:
132+
"""Get tasks by ids or statuses"""
133+
return to_sync(self.queue_get_tasks_async)(jobs, status)
134+
135+
async def queue_get_task_async(self, task_id: int) -> Optional[Mapping[str, Any]]:
136+
"""Get task by id"""
137+
for task_dict in await self.queue_get_tasks_async(jobs=[task_id]):
138+
return task_dict
139+
85140
def queue_get_task(self, task_id: int) -> Optional[Mapping[str, Any]]:
86141
"""Get task by id"""
87142
for task_dict in self.queue_get_tasks(jobs=[task_id]):

0 commit comments

Comments
 (0)