matlab-proxy 0.18.2__py3-none-any.whl → 0.20.0__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 matlab-proxy might be problematic. Click here for more details.

Files changed (36) hide show
  1. matlab_proxy/app.py +73 -55
  2. matlab_proxy/app_state.py +370 -155
  3. matlab_proxy/constants.py +3 -0
  4. matlab_proxy/gui/asset-manifest.json +6 -6
  5. matlab_proxy/gui/index.html +1 -1
  6. matlab_proxy/gui/static/css/{main.47712126.css → main.da9c4eb8.css} +2 -2
  7. matlab_proxy/gui/static/css/main.da9c4eb8.css.map +1 -0
  8. matlab_proxy/gui/static/js/{main.5b5ca2f2.js → main.9c68c75c.js} +3 -3
  9. matlab_proxy/gui/static/js/main.9c68c75c.js.map +1 -0
  10. matlab_proxy/matlab/startup.m +0 -20
  11. matlab_proxy/settings.py +16 -1
  12. matlab_proxy/util/__init__.py +101 -1
  13. matlab_proxy/util/event_loop.py +28 -10
  14. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  15. matlab_proxy/util/mwi/embedded_connector/helpers.py +9 -0
  16. matlab_proxy/util/mwi/embedded_connector/request.py +51 -21
  17. matlab_proxy/util/mwi/environment_variables.py +5 -0
  18. matlab_proxy/util/mwi/exceptions.py +16 -1
  19. matlab_proxy/util/mwi/logger.py +22 -2
  20. matlab_proxy/util/mwi/validators.py +33 -0
  21. matlab_proxy/util/windows.py +4 -1
  22. {matlab_proxy-0.18.2.dist-info → matlab_proxy-0.20.0.dist-info}/METADATA +15 -15
  23. {matlab_proxy-0.18.2.dist-info → matlab_proxy-0.20.0.dist-info}/RECORD +34 -34
  24. {matlab_proxy-0.18.2.dist-info → matlab_proxy-0.20.0.dist-info}/WHEEL +1 -1
  25. tests/unit/test_app.py +45 -22
  26. tests/unit/test_app_state.py +404 -111
  27. tests/unit/test_constants.py +1 -0
  28. tests/unit/util/mwi/test_logger.py +38 -4
  29. tests/unit/util/mwi/test_validators.py +30 -1
  30. tests/unit/util/test_util.py +83 -0
  31. matlab_proxy/gui/static/css/main.47712126.css.map +0 -1
  32. matlab_proxy/gui/static/js/main.5b5ca2f2.js.map +0 -1
  33. /matlab_proxy/gui/static/js/{main.5b5ca2f2.js.LICENSE.txt → main.9c68c75c.js.LICENSE.txt} +0 -0
  34. {matlab_proxy-0.18.2.dist-info → matlab_proxy-0.20.0.dist-info}/LICENSE.md +0 -0
  35. {matlab_proxy-0.18.2.dist-info → matlab_proxy-0.20.0.dist-info}/entry_points.txt +0 -0
  36. {matlab_proxy-0.18.2.dist-info → matlab_proxy-0.20.0.dist-info}/top_level.txt +0 -0
@@ -1,25 +1,5 @@
1
1
  % Copyright 2020-2024 The MathWorks, Inc.
2
2
 
3
- % Configure logged in user if possible
4
- if ~isempty(getenv('MW_LOGIN_USER_ID'))
5
- user_id = getenv('MW_LOGIN_USER_ID');
6
- first_name = getenv('MW_LOGIN_FIRST_NAME');
7
- last_name = getenv('MW_LOGIN_LAST_NAME');
8
- email_address = getenv('MW_LOGIN_EMAIL_ADDRESS');
9
- profile_id = getenv('MW_LOGIN_PROFILE_ID');
10
- display_name = getenv('MW_LOGIN_DISPLAY_NAME');
11
-
12
- li = com.mathworks.matlab_login.MatlabLogin.isUserLoggedIn(2, 'DESKTOP');
13
- token = li.getToken();
14
- login_level = 2;
15
- remember_me = true;
16
- li = com.mathworks.matlab_login.MatlabLogin.saveCacheLoginInfo(first_name, ...
17
- last_name, email_address, user_id, token, profile_id, login_level, ...
18
- remember_me, email_address, display_name);
19
- % Clear all local variables from users workspace.
20
- clear li login_level user_id first_name last_name email_address profile_id display_name token remember_me
21
- end
22
-
23
3
  if (strlength(getenv('MWI_BASE_URL')) > 0)
24
4
  connector.internal.setConfig('contextRoot', getenv('MWI_BASE_URL'));
25
5
  end
matlab_proxy/settings.py CHANGED
@@ -194,6 +194,7 @@ def get_dev_settings(config):
194
194
  "mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
195
195
  "mwi_custom_http_headers": mwi.custom_http_headers.get(),
196
196
  "env_config": mwi.validators.validate_env_config(config),
197
+ "integration_name": "MATLAB Desktop",
197
198
  "ssl_context": None,
198
199
  "mwi_logs_root_dir": get_mwi_logs_root_dir(dev=True),
199
200
  "mw_context_tags": get_mw_context_tags(matlab_proxy.get_default_config_name()),
@@ -209,6 +210,7 @@ def get_dev_settings(config):
209
210
  ),
210
211
  "warnings": [],
211
212
  "is_xvfb_available": False,
213
+ "mwi_idle_timeout": None,
212
214
  }
213
215
 
214
216
 
@@ -301,6 +303,14 @@ def get_server_settings(config_name):
301
303
  # log file validation check is already done in logger.py
302
304
  mwi_log_file = os.getenv(mwi_env.get_env_name_log_file(), None)
303
305
 
306
+ env_config = mwi.validators.validate_env_config(config_name)
307
+ short_desc = env_config["extension_name_short_description"]
308
+ integration_name = (
309
+ short_desc
310
+ if env_config["extension_name"] == matlab_proxy.get_default_config_name()
311
+ else f"{short_desc} - MATLAB Integration"
312
+ )
313
+
304
314
  return {
305
315
  "create_xvfb_cmd": create_xvfb_cmd,
306
316
  "base_url": mwi.validators.validate_base_url(
@@ -312,7 +322,8 @@ def get_server_settings(config_name):
312
322
  "app_port": mwi.validators.validate_app_port_is_free(
313
323
  os.getenv(mwi_env.get_env_name_app_port())
314
324
  ),
315
- "env_config": mwi.validators.validate_env_config(config_name),
325
+ "env_config": env_config,
326
+ "integration_name": integration_name,
316
327
  "mwapikey": str(uuid.uuid4()),
317
328
  "matlab_protocol": "https",
318
329
  "matlab_config_file": mwi_config_folder / "proxy_app_config.json",
@@ -334,6 +345,10 @@ def get_server_settings(config_name):
334
345
  os.getenv(mwi_env.get_env_name_mwi_use_existing_license(), "")
335
346
  ),
336
347
  "ssl_context": _validate_ssl_files_and_get_ssl_context(mwi_config_folder),
348
+ # validate_idle_timeout converts the timeout from minutes to seconds
349
+ "mwi_idle_timeout": mwi.validators.validate_idle_timeout(
350
+ os.getenv(mwi_env.get_env_name_shutdown_on_idle_timeout())
351
+ ),
337
352
  }
338
353
 
339
354
 
@@ -1,6 +1,6 @@
1
1
  # Copyright 2020-2024 The MathWorks, Inc.
2
-
3
2
  import argparse
3
+ import inspect
4
4
  import os
5
5
  import socket
6
6
  import time
@@ -15,6 +15,11 @@ from matlab_proxy.util.mwi.exceptions import (
15
15
  UIVisibleFatalError,
16
16
  )
17
17
 
18
+ from matlab_proxy.util.mwi.exceptions import (
19
+ LockAcquisitionError,
20
+ )
21
+
22
+
18
23
  logger = mwi.logger.get()
19
24
 
20
25
  # Global value to detect whether interrupt signal handler has been triggered or not.
@@ -276,3 +281,98 @@ def is_valid_path(path: Path):
276
281
  """
277
282
  path = Path(path)
278
283
  return path.is_dir() or path.is_file()
284
+
285
+
286
+ def get_caller_name() -> str:
287
+ """Utility function that uses the `inspect` module to access the call stack and returns the name
288
+ of the function that is two levels above in the stack. This is typically the function
289
+ that called the function that invoked `get_caller_name`.
290
+
291
+ Ex: start_matlab() -> set_matlab_state() -> get_caller_name()
292
+ The return value from get_caller_name() would be `start_matlab`
293
+
294
+ Returns:
295
+ str: Name of the parent function.
296
+ """
297
+ try:
298
+ return inspect.stack()[2][3]
299
+
300
+ except Exception as err:
301
+ logger.error(f"Failed to get caller name with err:{err}")
302
+ stack = inspect.stack()
303
+ return stack[len(stack) - 1][3]
304
+
305
+
306
+ class TrackingLock:
307
+ """A class which provides the same features as asyncio.Lock
308
+ and additionally tracks which function acquired the lock.
309
+ """
310
+
311
+ def __init__(self, purpose):
312
+ self._acquired_by = None
313
+ self._lock = asyncio.Lock()
314
+ if not purpose:
315
+ logger.warn("Provide a purpose for this instance of TrackingLock")
316
+ self._purpose = purpose
317
+
318
+ @property
319
+ def acquired_by(self):
320
+ return self._acquired_by
321
+
322
+ @property
323
+ def purpose(self):
324
+ return self._purpose
325
+
326
+ def locked(self):
327
+ return self._lock.locked()
328
+
329
+ async def acquire(self):
330
+ """Acquires the lock"""
331
+ await self._lock.acquire()
332
+ # Store the current task or function information when the lock is acquired
333
+ self._acquired_by = get_caller_name()
334
+ logger.debug(f"Lock acquired by '{self.acquired_by}()'")
335
+
336
+ async def release(self):
337
+ """Releases the lock."""
338
+ if self.locked():
339
+ # Clear the owner information when the lock is released
340
+ self._lock.release()
341
+ logger.debug(f"Lock released by '{self.acquired_by}()'")
342
+ self._acquired_by = None
343
+
344
+ else:
345
+ logger.warn(f"Trying to release {self._purpose} lock before acquiring it.")
346
+
347
+ def validate_lock_for_caller(self, caller):
348
+ """Checks if the specified caller is the current holder of the lock.
349
+
350
+ This method first verifies that the lock is currently held. If it is,
351
+ it then compares the holder's name with the provided caller's name
352
+ to ensure they match.
353
+
354
+ This function should be used by setter methods for verification, before they modify critical sections.
355
+
356
+ Args:
357
+ caller (str): The name of the function to be matched with the lock holder.
358
+
359
+ Returns:
360
+ bool: True if the caller currently holds the lock, False otherwise.
361
+ """
362
+ if not self._lock.locked():
363
+ logger.error(
364
+ LockAcquisitionError(
365
+ f"Lock needs to be acquired by '{caller}()' before modifying {self._purpose}"
366
+ )
367
+ )
368
+ return False
369
+
370
+ if self._acquired_by != caller:
371
+ logger.error(
372
+ LockAcquisitionError(
373
+ f"Lock was acquired by {self._acquired_by} but {self._purpose} is being modified by {caller}."
374
+ )
375
+ )
376
+ return False
377
+
378
+ return True
@@ -1,8 +1,10 @@
1
- # Copyright 2020-2022 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
+
3
+ from typing import Dict, Set, Union
4
+ from contextlib import suppress
2
5
 
3
6
  import asyncio
4
7
 
5
- from matlab_proxy import util
6
8
  from matlab_proxy.util import mwi, system, windows
7
9
 
8
10
  logger = mwi.logger.get()
@@ -30,16 +32,32 @@ def get_event_loop():
30
32
  return loop
31
33
 
32
34
 
33
- async def cancel_tasks(tasks):
35
+ async def cancel_tasks(tasks: Union[Dict[str, asyncio.Task], Set[asyncio.Task]]):
34
36
  """Cancels asyncio tasks.
35
37
 
36
38
  Args:
37
- tasks (asyncio.Task): Asyncio task
39
+ tasks: If a Dict[str, asyncio.Task], contains (task_name, task) as entries.
40
+ If a Set[asyncio.Task], contains a set of asyncio.Task objects.
41
+ """
42
+ if isinstance(tasks, dict):
43
+ for name, task in list(tasks.items()):
44
+ if task:
45
+ await __cancel_task(task)
46
+ logger.debug(f"{name} task stopped successfully")
47
+
48
+ elif isinstance(tasks, set):
49
+ for task in tasks:
50
+ if task:
51
+ await __cancel_task(task)
52
+ logger.debug(f"Task stopped successfully")
53
+
54
+
55
+ async def __cancel_task(task):
56
+ """Cancels a given asyncio task, suppressing CancelledError.
57
+
58
+ Args:
59
+ task (asyncio.Task): The asyncio task to be cancelled.
38
60
  """
39
- for task in tasks:
40
- logger.debug(f"Calling cancel on task: {task}")
61
+ with suppress(asyncio.CancelledError):
41
62
  task.cancel()
42
- try:
43
- await task
44
- except asyncio.CancelledError:
45
- pass
63
+ await task
@@ -1,4 +1,4 @@
1
1
  # Copyright (c) 2020-2022 The MathWorks, Inc.
2
2
 
3
3
  from . import helpers
4
- from .request import get_state, send_request
4
+ from .request import get_busy_state, send_request
@@ -46,6 +46,15 @@ def get_data_for_ping_request():
46
46
  return {"messages": {"Ping": [{}]}}
47
47
 
48
48
 
49
+ def get_data_for_matlab_busy_status_request():
50
+ """Returns data required to send in the payload for a MATLAB busy/idle status request to the embedded connector
51
+
52
+ Returns:
53
+ dict: Payload data
54
+ """
55
+ return {"messages": {"GetMatlabStatus": [{}]}}
56
+
57
+
49
58
  def get_data_to_eval_mcode(m_code):
50
59
  """Returns the data required to send in the payload for evaluating mcode using eval function to the embedded connector.
51
60
 
@@ -12,7 +12,11 @@ from matlab_proxy.util import mwi
12
12
 
13
13
  logger = mwi.logger.get()
14
14
 
15
- from .helpers import get_data_for_ping_request, get_ping_endpoint
15
+ from .helpers import (
16
+ get_data_for_ping_request,
17
+ get_data_for_matlab_busy_status_request,
18
+ get_ping_endpoint,
19
+ )
16
20
 
17
21
 
18
22
  async def send_request(url: str, data: dict, method: str, headers: dict = None) -> dict:
@@ -84,30 +88,56 @@ async def get_state(mwi_server_url, headers=None):
84
88
  headers=headers,
85
89
  )
86
90
 
87
- # Additional assert statements to catch any changes in response from embedded connector
88
- # Tested from R2020b to R2023a
89
- assert (
90
- "messages" in resp
91
- ), '"messages" key missing in response from embedded connector'
92
- assert (
93
- "PingResponse" in resp["messages"]
94
- ), '"PingResponse" key missing in response from embedded connector'
95
- assert (
96
- type(resp["messages"]["PingResponse"]) == list
97
- ), 'Was expecting an array in the "PingResponse" field in response'
98
- assert (
99
- len(resp["messages"]["PingResponse"]) == 1
100
- ), 'Was expecting an array of length 1 in the "PingResponse" field in response'
101
- assert (
102
- "messageFaults" in resp["messages"]["PingResponse"][0]
103
- ), 'Was expecting "messageFaults" field in response'
104
-
91
+ # Any changes in response from embedded connector would be caught by KeyError
105
92
  if not resp["messages"]["PingResponse"][0]["messageFaults"]:
106
93
  return "up"
94
+
95
+ except KeyError as key_err:
96
+ logger.error(f"Invalid Key Usage Detected! Check key: {key_err}")
97
+
107
98
  except Exception as err:
108
99
  logger.debug(
109
- f"{err}: Embedded connector is currently not responding to ping requests."
100
+ f"{err}: Embbeded connector is currently not responding to ping requests."
110
101
  )
111
- pass
112
102
 
113
103
  return "down"
104
+
105
+
106
+ async def get_busy_state(mwi_server_url, headers=None):
107
+ """Returns the state of MATLAB's Embedded Connector.
108
+
109
+ Args:
110
+ port (int): The port on which the embedded connector is running at
111
+ headers: Headers to include with the request
112
+ Returns:
113
+ str: Either "idle" or "busy" when a valid response is received. Else None is returned.
114
+ """
115
+ data = get_data_for_matlab_busy_status_request()
116
+ url = get_ping_endpoint(mwi_server_url)
117
+
118
+ busy_status = None
119
+
120
+ try:
121
+ resp = await send_request(
122
+ url=url,
123
+ data=data,
124
+ method="POST",
125
+ headers=headers,
126
+ )
127
+
128
+ busy_status = resp["messages"]["GetMatlabStatusResponse"][0]["status"].lower()
129
+
130
+ assert busy_status in [
131
+ "idle",
132
+ "busy",
133
+ ], f"Was expecting MATLAB busy status to be either 'idle' or 'busy', but received {busy_status} instead."
134
+
135
+ except KeyError as key_err:
136
+ logger.error(f"Invalid Key Usage Detected! Check key: {key_err}")
137
+
138
+ except Exception as err:
139
+ logger.debug(
140
+ f"{err}: Embedded connector is currently not responding to ping requests."
141
+ )
142
+
143
+ return busy_status
@@ -167,6 +167,11 @@ def get_env_name_custom_matlab_code():
167
167
  return "MWI_MATLAB_STARTUP_SCRIPT"
168
168
 
169
169
 
170
+ def get_env_name_shutdown_on_idle_timeout():
171
+ """User specified timeout in minutes for shutdown on idle of matlab-proxy"""
172
+ return "MWI_SHUTDOWN_ON_IDLE_TIMEOUT"
173
+
174
+
170
175
  class Experimental:
171
176
  """This class houses functions which are undocumented APIs and Environment variables.
172
177
  Note: Never add any state to this class. Its only intended for use as an abstraction layer
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2020-2023 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
2
 
3
3
 
4
4
  class AppError(Exception):
@@ -165,6 +165,21 @@ class InvalidTokenError(AppError):
165
165
  pass
166
166
 
167
167
 
168
+ class LockAcquisitionError(Exception):
169
+ """Exception raised when a lock is not properly acquired before modifying a variable.
170
+
171
+ This error is thrown in scenarios where:
172
+ 1) A lock must be acquired before modifying a shared resource, but it wasn't.
173
+ 2) The lock for a shared resource was acquired by one function, but another function attempts to modify the resource without holding the lock.
174
+
175
+
176
+ Args:
177
+ Exception : Python's inbuilt Exception Class.
178
+ """
179
+
180
+ pass
181
+
182
+
168
183
  def log_error(logger, err: Exception):
169
184
  """Logs any error to stdout.
170
185
 
@@ -1,4 +1,4 @@
1
- # Copyright 2020-2023 The MathWorks, Inc.
1
+ # Copyright 2020-2024 The MathWorks, Inc.
2
2
  """Functions to access & control the logging behavior of the app
3
3
  """
4
4
 
@@ -52,7 +52,17 @@ def __set_logging_configuration():
52
52
  # query for user specified environment variables
53
53
  log_level = os.getenv(
54
54
  mwi_env.get_env_name_logging_level(), __get_default_log_level()
55
- )
55
+ ).upper()
56
+
57
+ valid = __is_valid_log_level(log_level)
58
+
59
+ if not valid:
60
+ default_log_level = __get_default_log_level()
61
+ logging.warn(
62
+ f"Unknown log level '{log_level}' set. Defaulting to log level '{default_log_level}'..."
63
+ )
64
+ log_level = default_log_level
65
+
56
66
  log_file = os.getenv(mwi_env.get_env_name_log_file(), None)
57
67
 
58
68
  ## Set logging object
@@ -110,3 +120,13 @@ def __get_default_log_level():
110
120
  String: The default logging level
111
121
  """
112
122
  return "INFO"
123
+
124
+
125
+ def __is_valid_log_level(log_level):
126
+ """Helper to check if the log level is valid.
127
+
128
+ Returns:
129
+ Boolean: Whether log level exists
130
+ """
131
+
132
+ return hasattr(logging, log_level)
@@ -12,6 +12,7 @@ Exceptions are thrown to signal failure.
12
12
  """
13
13
 
14
14
  import errno
15
+ import math
15
16
  import os
16
17
  import socket
17
18
  from pathlib import Path
@@ -344,3 +345,35 @@ def validate_matlab_root_path(matlab_root: Path, is_custom_matlab_root: bool):
344
345
  return None
345
346
 
346
347
  return matlab_root
348
+
349
+
350
+ def validate_idle_timeout(timeout):
351
+ """Validate if IDLE timeout for matlab-proxy
352
+
353
+ Args:
354
+ timeout (None | int): IDLE timeout for shutdown of matlab-proxy.
355
+
356
+ Raises:
357
+ ValueError: If a non-numerical value is supplied other than None.
358
+
359
+ Returns:
360
+ None | int : The timeout value.
361
+ """
362
+ if not timeout:
363
+ return timeout
364
+
365
+ try:
366
+ # Convert timeout to seconds
367
+ timeout = math.ceil(float(timeout) * 60)
368
+
369
+ if timeout <= 0:
370
+ raise ValueError
371
+
372
+ logger.info(f"MATLAB IDLE timeout set to {timeout} seconds")
373
+ return timeout
374
+
375
+ except ValueError:
376
+ logger.warn(
377
+ f"Invalid value supplied for {mwi_env.get_env_name_shutdown_on_idle_timeout()}: {timeout}. Continuing without any IDLE timeout."
378
+ )
379
+ return None
@@ -49,10 +49,13 @@ async def start_matlab(matlab_cmd, matlab_env):
49
49
  """
50
50
  import psutil
51
51
 
52
+ # The stdout is used to suppress the MATLAB outputs from being shown in the terminal.
53
+ # We set it to DEVNULL instead of PIPE because PIPE has a limited buffer size and can
54
+ # block the process if the output exceeds the buffer limit.
52
55
  intermediate_proc = await asyncio.create_subprocess_exec(
53
56
  *matlab_cmd,
54
57
  env=matlab_env,
55
- stdout=asyncio.subprocess.PIPE,
58
+ stdout=asyncio.subprocess.DEVNULL,
56
59
  stderr=asyncio.subprocess.STDOUT,
57
60
  )
58
61
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: matlab-proxy
3
- Version: 0.18.2
3
+ Version: 0.20.0
4
4
  Summary: Python® package enables you to launch MATLAB® and access it from a web browser.
5
5
  Home-page: https://github.com/mathworks/matlab-proxy/
6
6
  Author: The MathWorks, Inc.
@@ -17,24 +17,24 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Requires-Python: ~=3.8
19
19
  Description-Content-Type: text/markdown
20
- Requires-Dist: aiohttp >=3.7.4
20
+ Requires-Dist: aiohttp>=3.7.4
21
21
  Requires-Dist: aiohttp-session[secure]
22
22
  Requires-Dist: importlib-metadata
23
23
  Requires-Dist: importlib-resources
24
24
  Requires-Dist: psutil
25
25
  Provides-Extra: dev
26
- Requires-Dist: aiohttp-devtools ; extra == 'dev'
27
- Requires-Dist: black ; extra == 'dev'
28
- Requires-Dist: pytest ; extra == 'dev'
29
- Requires-Dist: pytest-env ; extra == 'dev'
30
- Requires-Dist: pytest-cov ; extra == 'dev'
31
- Requires-Dist: pytest-timeout ; extra == 'dev'
32
- Requires-Dist: pytest-mock ; extra == 'dev'
33
- Requires-Dist: pytest-aiohttp ; extra == 'dev'
34
- Requires-Dist: psutil ; extra == 'dev'
35
- Requires-Dist: urllib3 ; extra == 'dev'
36
- Requires-Dist: requests ; extra == 'dev'
37
- Requires-Dist: pytest-playwright ; extra == 'dev'
26
+ Requires-Dist: aiohttp-devtools; extra == "dev"
27
+ Requires-Dist: black; extra == "dev"
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: pytest-env; extra == "dev"
30
+ Requires-Dist: pytest-cov; extra == "dev"
31
+ Requires-Dist: pytest-timeout; extra == "dev"
32
+ Requires-Dist: pytest-mock; extra == "dev"
33
+ Requires-Dist: pytest-aiohttp; extra == "dev"
34
+ Requires-Dist: psutil; extra == "dev"
35
+ Requires-Dist: urllib3; extra == "dev"
36
+ Requires-Dist: requests; extra == "dev"
37
+ Requires-Dist: pytest-playwright; extra == "dev"
38
38
 
39
39
  # MATLAB Proxy
40
40
  [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/mathworks/matlab-proxy/run-tests.yml?branch=main&logo=github)](https://github.com/mathworks/matlab-proxy/actions) &nbsp; [![PyPI badge](https://img.shields.io/pypi/v/matlab-proxy.svg?logo=pypi)](https://pypi.python.org/pypi/matlab-proxy) &nbsp; [![codecov](https://codecov.io/gh/mathworks/matlab-proxy/branch/main/graph/badge.svg?token=ZW3SESKCSS)](https://codecov.io/gh/mathworks/matlab-proxy) &nbsp; [![Downloads](https://static.pepy.tech/personalized-badge/matlab-proxy?period=month&units=international_system&left_color=grey&right_color=blue&left_text=PyPI%20downloads/month)](https://pepy.tech/project/matlab-proxy)
@@ -201,7 +201,7 @@ $ pip install --upgrade matlab-proxy>=0.5.0
201
201
 
202
202
  ### Windows Subsystem for Linux (WSL 2)
203
203
 
204
- To install `matlab-proxy` in WSL 2, follow the steps mentioned in the [Installation Guide for WSL 2](./installation/wsl2/README.md).
204
+ To install `matlab-proxy` in WSL 2, follow the steps mentioned in the [Installation Guide for WSL 2](./install_guides/wsl2/README.md).
205
205
 
206
206
  ## Using an already activated MATLAB with matlab-proxy
207
207
  `matlab-proxy` version `v0.7.0` introduces support for using an existing MATLAB license. Use the Existing License option only if you have an activated MATLAB. This allows you to start MATLAB without authenticating every time.