Skip to content

Commit 2f19a0d

Browse files
committed
add http client
1 parent 6765e1e commit 2f19a0d

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

cylc/uiserver/client.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
17+
import json
18+
import os
19+
import requests
20+
from shutil import which
21+
import socket
22+
import sys
23+
from typing import Any, Optional, Union, Dict
24+
25+
from cylc.flow import LOG
26+
from cylc.flow.exceptions import ClientError, ClientTimeout
27+
from cylc.flow.network import encode_
28+
from cylc.flow.network.client import WorkflowRuntimeClientBase
29+
from cylc.flow.network.client_factory import CommsMeth
30+
31+
from cylc.uiserver.app import API_INFO_FILE
32+
33+
34+
class WorkflowRuntimeClient(WorkflowRuntimeClientBase):
35+
"""Client to UI server communication using HTTP."""
36+
37+
DEFAULT_TIMEOUT = 10 # seconds
38+
39+
def __init__(
40+
self,
41+
workflow: str,
42+
host: Optional[str] = None,
43+
port: Union[int, str, None] = None,
44+
timeout: Union[float, str, None] = None,
45+
):
46+
self.timeout = timeout
47+
# gather header info post start
48+
self.header = self.get_header()
49+
50+
async def async_request(
51+
self,
52+
command: str,
53+
args: Optional[Dict[str, Any]] = None,
54+
timeout: Optional[float] = None,
55+
req_meta: Optional[Dict[str, Any]] = None
56+
) -> object:
57+
"""Send an asynchronous request using asyncio.
58+
59+
Has the same arguments and return values as ``serial_request``.
60+
61+
"""
62+
if not args:
63+
args = {}
64+
65+
with open(API_INFO_FILE, "r") as api_file:
66+
api_info = json.loads(api_file.read())
67+
68+
# send message
69+
msg: Dict[str, Any] = {'command': command, 'args': args}
70+
msg.update(self.header)
71+
# add the request metadata
72+
if req_meta:
73+
msg['meta'].update(req_meta)
74+
75+
LOG.debug('http:send %s', msg)
76+
77+
try:
78+
res = requests.post(
79+
api_info["url"] + 'cylc/graphql',
80+
headers={
81+
'Authorization': f'token {api_info["token"]}',
82+
'meta': encode_(msg.get('meta', {})),
83+
},
84+
json={
85+
'query': args['request_string'],
86+
'variables': args.get('variables', {}),
87+
},
88+
timeout=self.timeout
89+
)
90+
res.raise_for_status()
91+
except requests.ConnectTimeout:
92+
raise ClientTimeout(
93+
'Timeout waiting for server response.'
94+
' This could be due to network or server issues.'
95+
' Check the UI Server log.'
96+
)
97+
except requests.ConnectionError as exc:
98+
raise ClientError(
99+
'Unable to connect to UI Server or Hub.',
100+
f'{exc}'
101+
)
102+
103+
response = res.json()
104+
LOG.debug('http:recv %s', response)
105+
106+
try:
107+
return response['data']
108+
except KeyError:
109+
error = response.get(
110+
'error',
111+
{'message': f'Received invalid response: {response}'},
112+
)
113+
raise ClientError(
114+
error.get('message'),
115+
error.get('traceback'),
116+
)
117+
118+
def get_header(self) -> dict:
119+
"""Return "header" data to attach to each request for traceability.
120+
121+
Returns:
122+
dict: dictionary with the header information, such as
123+
program and hostname.
124+
"""
125+
host = socket.gethostname()
126+
if len(sys.argv) > 1:
127+
cmd = sys.argv[1]
128+
else:
129+
cmd = sys.argv[0]
130+
131+
cylc_executable_location = which("cylc")
132+
if cylc_executable_location:
133+
cylc_bin_dir = os.path.abspath(
134+
os.path.join(cylc_executable_location, os.pardir)
135+
)
136+
if not cylc_bin_dir.endswith("/"):
137+
cylc_bin_dir = f"{cylc_bin_dir}/"
138+
139+
if cmd.startswith(cylc_bin_dir):
140+
cmd = cmd.replace(cylc_bin_dir, '')
141+
return {
142+
'meta': {
143+
'prog': cmd,
144+
'host': host,
145+
'comms_method':
146+
os.getenv(
147+
"CLIENT_COMMS_METH",
148+
default=CommsMeth.HTTP.value
149+
)
150+
}
151+
}

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ install_requires =
5757
jupyter_server>=1.10.2
5858
tornado>=6.1.0 # matches jupyter_server value
5959
traitlets>=5.2.1 # required for logging_config (5.2.0 had bugs)
60+
requests==2.28.*
6061

6162
# Transitive dependencies that we directly (lightly) use:
6263
pyzmq

0 commit comments

Comments
 (0)