edq-utils 0.2.3__py3-none-any.whl → 0.2.4__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.

Potentially problematic release.


This version of edq-utils might be problematic. Click here for more details.

edq/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  General Python tools used by several EduLinq projects.
3
3
  """
4
4
 
5
- __version__ = '0.2.3'
5
+ __version__ = '0.2.4'
@@ -14,14 +14,14 @@ import edq.util.net
14
14
  class ExchangeVerification(edq.testing.unittest.BaseTest):
15
15
  """ Verify that exchanges match their content. """
16
16
 
17
- def run(paths: typing.List[str], server: str) -> int:
17
+ def run(paths: typing.List[str], server: str, fail_fast: bool = False) -> int:
18
18
  """ Run exchange verification. """
19
19
 
20
20
  exchange_paths = _collect_exchange_paths(paths)
21
21
 
22
22
  _attach_tests(exchange_paths, server)
23
23
 
24
- runner = unittest.TextTestRunner(verbosity = 2)
24
+ runner = unittest.TextTestRunner(verbosity = 2, failfast = fail_fast)
25
25
  tests = unittest.defaultTestLoader.loadTestsFromTestCase(ExchangeVerification)
26
26
  results = runner.run(tests)
27
27
 
edq/testing/run.py CHANGED
@@ -64,7 +64,7 @@ def run(args: typing.Union[argparse.Namespace, typing.Dict[str, typing.Any], Non
64
64
  if (len(test_dirs) == 0):
65
65
  test_dirs.append('.')
66
66
 
67
- runner = unittest.TextTestRunner(verbosity = 3)
67
+ runner = unittest.TextTestRunner(verbosity = 3, failfast = args.get('fail_fast', False))
68
68
  test_cases = []
69
69
 
70
70
  for test_dir in test_dirs:
@@ -0,0 +1,284 @@
1
+ import argparse
2
+ import atexit
3
+ import logging
4
+ import signal
5
+ import subprocess
6
+ import time
7
+ import typing
8
+
9
+ import edq.util.dirent
10
+ import edq.util.net
11
+
12
+ DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS: float = 0.2
13
+ DEFAULT_STARTUP_WAIT_SECS: float = 10.0
14
+ SERVER_STOP_WAIT_SECS: float = 5.00
15
+
16
+ DEFAULT_IDENTIFY_MAX_ATTEMPTS: int = 100
17
+ DEFAULT_IDENTIFY_WAIT_SECS: float = 0.25
18
+
19
+ _logger = logging.getLogger(__name__)
20
+
21
+ class ServerRunner():
22
+ """
23
+ A class for running an external HTTP server for some sort of larger process (like testing or generating data).
24
+ """
25
+
26
+ def __init__(self,
27
+ server: typing.Union[str, None] = None,
28
+ server_start_command: typing.Union[str, None] = None,
29
+ server_stop_command: typing.Union[str, None] = None,
30
+ http_exchanges_out_dir: typing.Union[str, None] = None,
31
+ server_output_path: typing.Union[str, None] = None,
32
+ startup_initial_wait_secs: float = DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS,
33
+ startup_wait_secs: typing.Union[float, None] = None,
34
+ startup_skip_identify: typing.Union[bool, None] = False,
35
+ identify_max_attempts: int = DEFAULT_IDENTIFY_MAX_ATTEMPTS,
36
+ identify_wait_secs: float = DEFAULT_IDENTIFY_WAIT_SECS,
37
+ **kwargs: typing.Any) -> None:
38
+ if (server is None):
39
+ raise ValueError('No server specified.')
40
+
41
+ self.server: str = server
42
+ """ The server address to point requests to. """
43
+
44
+ if (server_start_command is None):
45
+ raise ValueError('No command to start the server was specified.')
46
+
47
+ self.server_start_command: str = server_start_command
48
+ """ The server_start_command to run the LMS server. """
49
+
50
+ self.server_stop_command: typing.Union[str, None] = server_stop_command
51
+ """ An optional command to stop the server. """
52
+
53
+ if (http_exchanges_out_dir is None):
54
+ http_exchanges_out_dir = edq.util.dirent.get_temp_dir(prefix = 'edq-serverrunner-http-exchanges-', rm = False)
55
+
56
+ self.http_exchanges_out_dir: str = http_exchanges_out_dir
57
+ """ Where to output the HTTP exchanges. """
58
+
59
+ if (server_output_path is None):
60
+ server_output_path = edq.util.dirent.get_temp_path(prefix = 'edq-serverrunner-server-output-', rm = False) + '.txt'
61
+
62
+ self.server_output_path: str = server_output_path
63
+ """ Where to write server output (stdout and stderr). """
64
+
65
+ self.startup_initial_wait_secs: float = startup_initial_wait_secs
66
+ """ The duration to wait after giving the initial startup command. """
67
+
68
+ if (startup_wait_secs is None):
69
+ startup_wait_secs = DEFAULT_STARTUP_WAIT_SECS
70
+
71
+ self.startup_wait_secs = startup_wait_secs
72
+ """ How long to wait after the server start command is run before making requests to the server. """
73
+
74
+ if (startup_skip_identify is None):
75
+ startup_skip_identify = False
76
+
77
+ self.startup_skip_identify: bool = startup_skip_identify
78
+ """
79
+ Whether to skip trying to identify the server after it has been started.
80
+ This acts as a way to have a variable wait for the server to start.
81
+ When not used, self.startup_wait_secs is the only way to wait for the server to start.
82
+ """
83
+
84
+ self.identify_max_attempts: int = identify_max_attempts
85
+ """ The maximum number of times to try an identity check before starting the server. """
86
+
87
+ self.identify_wait_secs: float = identify_wait_secs
88
+ """ The number of seconds each identify request will wait for the server to respond. """
89
+
90
+ self._old_exchanges_out_dir: typing.Union[str, None] = None
91
+ """
92
+ The value of edq.util.net._exchanges_out_dir when start() is called.
93
+ The original value may be changed in start(), and will be reset in stop().
94
+ """
95
+
96
+ self._process: typing.Union[subprocess.Popen, None] = None
97
+ """ The server process. """
98
+
99
+ self._server_output_file: typing.Union[typing.IO, None] = None
100
+ """ The file that server output is written to. """
101
+
102
+ def start(self) -> None:
103
+ """ Start the server. """
104
+
105
+ if (self._process is not None):
106
+ return
107
+
108
+ # Ensure stop() is called.
109
+ atexit.register(self.stop)
110
+
111
+ # Store and set networking config.
112
+
113
+ self._old_exchanges_out_dir = edq.util.net._exchanges_out_dir
114
+ edq.util.net._exchanges_out_dir = self.http_exchanges_out_dir
115
+
116
+ # Start the server.
117
+
118
+ _logger.info("Writing HTTP exchanges to '%s'.", self.http_exchanges_out_dir)
119
+ _logger.info("Writing server output to '%s'.", self.server_output_path)
120
+ _logger.info("Starting the server ('%s') and waiting for it.", self.server)
121
+
122
+ self._server_output_file = open(self.server_output_path, 'a', encoding = edq.util.dirent.DEFAULT_ENCODING) # pylint: disable=consider-using-with
123
+
124
+ self._start_server()
125
+ _logger.info("Server is started up.")
126
+
127
+ def _start_server(self) -> None:
128
+ """ Start the server. """
129
+
130
+ if (self._process is not None):
131
+ return
132
+
133
+ self._process = subprocess.Popen(self.server_start_command, # pylint: disable=consider-using-with
134
+ shell = True, stdout = self._server_output_file, stderr = subprocess.STDOUT)
135
+
136
+ status = None
137
+ try:
138
+ # Wait for a short period for the process to start.
139
+ status = self._process.wait(self.startup_initial_wait_secs)
140
+ except subprocess.TimeoutExpired:
141
+ # Good, the server is running.
142
+ pass
143
+
144
+ if (status is not None):
145
+ hint = f"code: '{status}'"
146
+ if (status == 125):
147
+ hint = 'server may already be running'
148
+
149
+ raise ValueError(f"Server was unable to start successfully ('{hint}').")
150
+
151
+ _logger.info("Completed initial server start wait.")
152
+
153
+ # Ping the server to check if it has started.
154
+ if (not self.startup_skip_identify):
155
+ for _ in range(self.identify_max_attempts):
156
+ if (self.identify_server()):
157
+ # The server is running and responding, exit early.
158
+ return
159
+
160
+ time.sleep(self.identify_wait_secs)
161
+
162
+ status = None
163
+ try:
164
+ # Ensure the server is running cleanly.
165
+ status = self._process.wait(self.startup_wait_secs)
166
+ except subprocess.TimeoutExpired:
167
+ # Good, the server is running.
168
+ pass
169
+
170
+ if (status is not None):
171
+ raise ValueError(f"Server was unable to start successfully ('code: {status}').")
172
+
173
+ def stop(self) -> bool:
174
+ """
175
+ Stop the server.
176
+ Return true if child classes should perform shutdown behavior.
177
+ """
178
+
179
+ if (self._process is None):
180
+ return False
181
+
182
+ # Stop the server.
183
+ _logger.info('Stopping the server.')
184
+ self._stop_server()
185
+
186
+ # Restore networking config.
187
+
188
+ edq.util.net._exchanges_out_dir = self._old_exchanges_out_dir
189
+ self._old_exchanges_out_dir = None
190
+
191
+ if (self._server_output_file is not None):
192
+ self._server_output_file.close()
193
+ self._server_output_file = None
194
+
195
+ return True
196
+
197
+ def restart(self) -> None:
198
+ """ Restart the server. """
199
+
200
+ _logger.debug('Restarting the server.')
201
+ self._stop_server()
202
+ self._start_server()
203
+
204
+ def identify_server(self) -> bool:
205
+ """
206
+ Attempt to identify the target server and return true on a successful attempt.
207
+ This is used on startup to wait for the server to complete startup.
208
+
209
+ Child classes must implement this or set self.startup_skip_identify to true.
210
+ """
211
+
212
+ raise NotImplementedError('identify_server')
213
+
214
+ def _stop_server(self) -> typing.Union[int, None]:
215
+ """ Stop the server process and return the exit status. """
216
+
217
+ if (self._process is None):
218
+ return None
219
+
220
+ # Mark the process as dead, so it can be restarted (if need be).
221
+ current_process = self._process
222
+ self._process = None
223
+
224
+ # Check if the process is already dead.
225
+ status = current_process.poll()
226
+ if (status is not None):
227
+ return status
228
+
229
+ # If the user provided a special command, try it.
230
+ if (self.server_stop_command is not None):
231
+ subprocess.run(self.server_stop_command,
232
+ shell = True, stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL,
233
+ check = False)
234
+
235
+ status = current_process.poll()
236
+ if (status is not None):
237
+ return status
238
+
239
+ # Try to end the server gracefully.
240
+ try:
241
+ current_process.send_signal(signal.SIGINT)
242
+ current_process.wait(SERVER_STOP_WAIT_SECS)
243
+ except subprocess.TimeoutExpired:
244
+ pass
245
+
246
+ status = current_process.poll()
247
+ if (status is not None):
248
+ return status
249
+
250
+ # End the server hard.
251
+ try:
252
+ current_process.kill()
253
+ current_process.wait(SERVER_STOP_WAIT_SECS)
254
+ except subprocess.TimeoutExpired:
255
+ pass
256
+
257
+ status = current_process.poll()
258
+ if (status is not None):
259
+ return status
260
+
261
+ return None
262
+
263
+ def modify_parser(parser: argparse.ArgumentParser) -> None:
264
+ """ Modify the parser to add arguments for running a server. """
265
+
266
+ parser.add_argument('server_start_command', metavar = 'RUN_SERVER_COMMAND',
267
+ action = 'store', type = str,
268
+ help = 'The command to run the LMS server that will be the target of the data generation commands.')
269
+
270
+ parser.add_argument('--startup-skip-identify', dest = 'startup_skip_identify',
271
+ action = 'store_true', default = False,
272
+ help = 'If set, startup will skip trying to identify the server as a means of checking that the server is started.')
273
+
274
+ parser.add_argument('--startup-wait', dest = 'startup_wait_secs',
275
+ action = 'store', type = float, default = DEFAULT_STARTUP_WAIT_SECS,
276
+ help = 'The time to wait between starting the server and sending commands (default: %(default)s).')
277
+
278
+ parser.add_argument('--server-output-file', dest = 'server_output_path',
279
+ action = 'store', type = str, default = None,
280
+ help = 'Where server output will be written. Defaults to a random temp file.')
281
+
282
+ parser.add_argument('--server-stop-command', dest = 'server_stop_command',
283
+ action = 'store', type = str, default = None,
284
+ help = 'An optional command to stop the server. After this the server will be sent a SIGINT and then a SIGKILL.')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edq-utils
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Common utilities used by EduLinq Python projects.
5
5
  Author-email: Eriq Augustine <eriq@edulinq.org>
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- edq/__init__.py,sha256=1nvGwN3NcZoLaMn492OHIRJXa3iLUjUOuE4Vi9rtvxI,86
1
+ edq/__init__.py,sha256=BQj0xWz308BiTV2bY3aZN7iCSeJsyCVx9U0CvhJ5JyY,86
2
2
  edq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  edq/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  edq/cli/__main__.py,sha256=WvJPPNDCNI5dxco9tFWkTmBRCvhPSoqdiX9S8UFStZg,404
@@ -22,14 +22,15 @@ edq/core/config_test.py,sha256=I49YVB0iLalZ3yrqfLZTfRpI128htAjdYkFJp5IRDTg,36991
22
22
  edq/core/log.py,sha256=Aq5-Tznq5fgCfU0OI4ECy2OnCoiwV4bTFkhXXdLoG7Q,3726
23
23
  edq/core/version.py,sha256=cCadcn3M4qoxbQRu4WRHXUu_xl49GTGSDpr6hpr7Vxw,124
24
24
  edq/procedure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- edq/procedure/verify_exchanges.py,sha256=Bv-wscTaSx5bbQfD6tZutCtG_pyHuFIBhk_RB5ZMJEQ,2765
25
+ edq/procedure/verify_exchanges.py,sha256=EQ3dcrt8TxrGV6JFJFUHTzDYZwToqi-FQCK5mCZYsU0,2812
26
26
  edq/testing/__init__.py,sha256=IKd3fPU_8d_jP19HxG-zKwxFwn7nqFGGtXOY5slY41c,32
27
27
  edq/testing/asserts.py,sha256=BxWTH9aQFwmzb0tf5hx3V4aeJzmiBOO-QajaMM6zklI,2523
28
28
  edq/testing/cli.py,sha256=pkms1Gy1VzXjRp3bT53TlnxtQUMuN-OntvRNlcpd960,14088
29
29
  edq/testing/cli_test.py,sha256=IqzdK1fEit_3HagSU-nNI4EjkoQp6-I88JGZ1_x_uQk,525
30
30
  edq/testing/httpserver.py,sha256=VlzynAZ8JJ1tY_NF6Z6u_C9MBPqcmiSnyBxZpL_dQBw,21041
31
31
  edq/testing/httpserver_test.py,sha256=tjBgBbKTHeqE1ugHyao4HpW7FNPTkBGpWK3vuqJgNQg,12123
32
- edq/testing/run.py,sha256=Axv0t6yZRUS39xmf5PWR5b54Rg21Nz0ip8G4UyJm1Ik,4814
32
+ edq/testing/run.py,sha256=EQvMUYCSZdolw16V3mrAY7ZBq1dtlqGEHkTdG0cmLFQ,4855
33
+ edq/testing/serverrunner.py,sha256=AR3cIz1q69eAVzUzXzBTAmNQnRZDq5qAluStSSbvF10,10656
33
34
  edq/testing/unittest.py,sha256=7-5ScxXpMBvhhyCvRMPiiUbdfx9FcpMgU6M5A3oHV7c,2980
34
35
  edq/testing/testdata/cli/data/configs/empty/edq-config.json,sha256=yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y,3
35
36
  edq/testing/testdata/cli/data/configs/simple-1/edq-config.json,sha256=uQdt7o7VNjOSQk8ni6UrqZ95QkJTUgHcwaL2TvY30bs,42
@@ -81,8 +82,8 @@ edq/util/pyimport_test.py,sha256=Xno0MIa3yMTfBfoTgjKCIMpr1ZShU6bvo9rBRdecXQU,420
81
82
  edq/util/reflection.py,sha256=jPcW6h0fwSDYh04O5rUxlgoF7HK6fVQ2mq7DD9qPrEg,972
82
83
  edq/util/time.py,sha256=anoNM_KniARLombv2BnsoHuCzDqMKiDdIzV7RUe2ZOk,2648
83
84
  edq/util/time_test.py,sha256=iQZwzVTVQQ4TdXrLb9MUMCYlKrIe8qyF-hiC9YLTaMo,4610
84
- edq_utils-0.2.3.dist-info/licenses/LICENSE,sha256=MS4iYEl4rOxMoprZuc86iYVoyk4YgaVoMt7WmGvVF8w,1064
85
- edq_utils-0.2.3.dist-info/METADATA,sha256=848uXn4-ZBRMoEuG3ojYBVRb4vFqk5GxCH70yk51MX4,7535
86
- edq_utils-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
- edq_utils-0.2.3.dist-info/top_level.txt,sha256=znBHSj6tgXtcMKrUVtovLli5fIEJCb7d-BMxTLRK4zk,4
88
- edq_utils-0.2.3.dist-info/RECORD,,
85
+ edq_utils-0.2.4.dist-info/licenses/LICENSE,sha256=MS4iYEl4rOxMoprZuc86iYVoyk4YgaVoMt7WmGvVF8w,1064
86
+ edq_utils-0.2.4.dist-info/METADATA,sha256=kK7x225vjOz23C1qGKlS_nEBHqMsVCQZMl9ZgaEP41g,7535
87
+ edq_utils-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
88
+ edq_utils-0.2.4.dist-info/top_level.txt,sha256=znBHSj6tgXtcMKrUVtovLli5fIEJCb7d-BMxTLRK4zk,4
89
+ edq_utils-0.2.4.dist-info/RECORD,,