systemlink-cli 1.11.2__tar.gz → 1.11.5__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.
Files changed (88) hide show
  1. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/PKG-INFO +3 -1
  2. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/pyproject.toml +15 -11
  3. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/_version.py +1 -1
  4. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/main.py +3 -6
  5. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/notebook_click.py +0 -1
  6. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/platform.py +0 -1
  7. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/rich_output.py +0 -1
  8. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/routine_click.py +0 -1
  9. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/system_click.py +1 -0
  10. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/user_click.py +0 -1
  11. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/web_editor.py +62 -11
  12. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/workitem_click.py +0 -1
  13. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/LICENSE +0 -0
  14. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/dff-editor/editor.js +0 -0
  15. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/dff-editor/index.html +0 -0
  16. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/__init__.py +0 -0
  17. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/__main__.py +0 -0
  18. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/asset_click.py +0 -0
  19. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/cli_formatters.py +0 -0
  20. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/cli_utils.py +0 -0
  21. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/comment_click.py +0 -0
  22. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/completion_click.py +0 -0
  23. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/config.py +0 -0
  24. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/config_click.py +0 -0
  25. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/dff_click.py +0 -0
  26. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/dff_decorators.py +0 -0
  27. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/example_click.py +0 -0
  28. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/example_loader.py +0 -0
  29. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/example_provisioner.py +0 -0
  30. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/README.md +0 -0
  31. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/_schema/schema-v1.0.json +0 -0
  32. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/demo-complete-workflow/README.md +0 -0
  33. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
  34. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/demo-test-plans/README.md +0 -0
  35. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/demo-test-plans/config.yaml +0 -0
  36. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
  37. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
  38. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
  39. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
  40. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
  41. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
  42. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
  43. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
  44. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
  45. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  46. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/feed_click.py +0 -0
  47. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/file_click.py +0 -0
  48. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/function_click.py +0 -0
  49. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/function_templates.py +0 -0
  50. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/mcp_click.py +0 -0
  51. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/mcp_reachability.py +0 -0
  52. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/mcp_server.py +0 -0
  53. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/policy_click.py +0 -0
  54. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/policy_utils.py +0 -0
  55. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/profiles.py +0 -0
  56. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/response_handlers.py +0 -0
  57. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skill_click.py +0 -0
  58. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/nipkg-file-package/SKILL.md +0 -0
  59. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/slcli/SKILL.md +0 -0
  60. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
  61. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/slcli/references/commands.md +0 -0
  62. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/slcli/references/datasheet-workflow.md +0 -0
  63. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/slcli/references/filtering.md +0 -0
  64. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/slcli/references/troubleshooting.md +0 -0
  65. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-job-debugging/SKILL.md +0 -0
  66. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-notebook/SKILL.md +0 -0
  67. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-notebook/references/interfaces.md +0 -0
  68. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-notebook/references/notebook-patterns.md +0 -0
  69. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-python-test/SKILL.md +0 -0
  70. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
  71. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
  72. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
  73. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
  74. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
  75. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/spec_click.py +0 -0
  76. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/ssl_trust.py +0 -0
  77. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/system_query_utils.py +0 -0
  78. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/table_utils.py +0 -0
  79. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/tag_click.py +0 -0
  80. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/templates_click.py +0 -0
  81. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/testmonitor_click.py +0 -0
  82. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/universal_handlers.py +0 -0
  83. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/utils.py +0 -0
  84. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/webapp_click.py +0 -0
  85. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/workflow_preview.py +0 -0
  86. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/workflows_click.py +0 -0
  87. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/workspace_click.py +0 -0
  88. {systemlink_cli-1.11.2 → systemlink_cli-1.11.5}/slcli/workspace_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemlink-cli
3
- Version: 1.11.2
3
+ Version: 1.11.5
4
4
  Summary: SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates.
5
5
  License-File: LICENSE
6
6
  Author: Fred Visser
@@ -11,8 +11,10 @@ Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
13
  Requires-Dist: click (>=7.1.2)
14
+ Requires-Dist: cryptography (>=46.0.7)
14
15
  Requires-Dist: keyring (>=25.6.0,<26.0.0)
15
16
  Requires-Dist: packaging (>=21.0)
17
+ Requires-Dist: pygments (>=2.20.0)
16
18
  Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
17
19
  Requires-Dist: questionary (>=2.1.1,<3.0.0)
18
20
  Requires-Dist: requests (>=2.32.4,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "systemlink-cli"
3
- version = "1.11.2"
3
+ version = "1.11.5"
4
4
  description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
5
5
  authors = ["Fred Visser <fred.visser@emerson.com>"]
6
6
  packages = [{ include = "slcli" }]
@@ -14,6 +14,7 @@ include = [
14
14
  [tool.poetry.scripts]
15
15
  slcli = "slcli.__main__:cli"
16
16
  slcli-mcp = "slcli.mcp_server:main"
17
+ ni-python-styleguide = "scripts.styleguide:main"
17
18
  build-pyinstaller = "scripts.build_pyinstaller:main"
18
19
  update-version = "scripts.update_version:main"
19
20
  release-from-changes = "scripts.towncrier_release:main"
@@ -39,13 +40,25 @@ questionary = "^2.1.1"
39
40
  packaging = ">=21.0"
40
41
  rich = ">=13.7,<15"
41
42
  rich-click = ">=1.8,<2"
43
+ cryptography = ">=46.0.7"
44
+ pygments = ">=2.20.0"
42
45
 
43
46
 
44
47
  [tool.poetry.group.dev.dependencies]
45
48
  # Lint
46
- ni-python-styleguide = ">=0.4.3"
47
49
  cyclonedx-bom = "^7.0.0"
48
50
  setuptools = "<81" # pin to retain pkg_resources used by flake8-import-order
51
+ black = ">=26.3.1"
52
+ flake8 = ">=7.3.0"
53
+ flake8-black = ">=0.4.0"
54
+ flake8-docstrings = ">=1.7.0"
55
+ flake8-import-order = ">=0.19.2"
56
+ isort = ">=6.0.1,<9"
57
+ pep8-naming = ">=0.15.1"
58
+ lxml = ">=6.1.0"
59
+ pyjwt = ">=2.12.0"
60
+ python-dotenv = ">=1.2.2"
61
+ python-multipart = ">=0.0.26"
49
62
 
50
63
  # Type checking
51
64
  mypy = ">=1.0"
@@ -96,15 +109,6 @@ markers = [
96
109
  [tool.black]
97
110
  line-length = 100
98
111
 
99
- [tool.ni-python-styleguide]
100
- # D301 ("Use r""" if any backslashes in a docstring") is suppressed because
101
- # Click uses \b (backspace chr(8)) as a paragraph-formatting marker in docstrings
102
- # to prevent line-wrapping in --help output. This requires a regular """ string;
103
- # using r""" passes the literal two-char sequence \b which Click does not recognise.
104
-
105
- [tool.ni-python-styleguide.lint]
106
- extend_ignore = "D301"
107
-
108
112
  [tool.mypy]
109
113
  python_version = "3.13"
110
114
  files = "**/*.py"
@@ -1,4 +1,4 @@
1
1
  """Version information for slcli."""
2
2
 
3
3
  # This file is auto-generated. Do not edit manually.
4
- __version__ = "1.11.2"
4
+ __version__ = "1.11.5"
@@ -1,6 +1,7 @@
1
1
  """slcli entry points."""
2
2
 
3
3
  import json
4
+ import tomllib
4
5
  from pathlib import Path
5
6
  from types import ModuleType
6
7
  from typing import Optional
@@ -8,7 +9,6 @@ from typing import Optional
8
9
  import click as base_click
9
10
  import keyring
10
11
  import questionary
11
- import tomllib
12
12
 
13
13
  from .asset_click import register_asset_commands
14
14
  from .comment_click import register_comment_commands
@@ -21,13 +21,10 @@ from .file_click import register_file_commands
21
21
  from .function_click import register_function_commands
22
22
  from .mcp_click import register_mcp_commands
23
23
  from .notebook_click import register_notebook_commands
24
- from .platform import (
25
- get_platform_info,
26
- )
24
+ from .platform import get_platform_info
27
25
  from .policy_click import register_policy_commands
28
26
  from .profiles import set_profile_override
29
- from .rich_output import install_rich_output
30
- from .rich_output import render_table
27
+ from .rich_output import install_rich_output, render_table
31
28
  from .routine_click import register_routine_commands
32
29
  from .skill_click import register_skill_commands
33
30
  from .spec_click import register_spec_commands
@@ -33,7 +33,6 @@ from .utils import (
33
33
  )
34
34
  from .workspace_utils import get_effective_workspace, get_workspace_display_name
35
35
 
36
-
37
36
  # Predefined notebook interfaces - must be assigned exactly as shown
38
37
  PREDEFINED_NOTEBOOK_INTERFACES = [
39
38
  "Assets Grid",
@@ -16,7 +16,6 @@ import requests
16
16
 
17
17
  from .utils import ExitCodes, get_ssl_verify
18
18
 
19
-
20
19
  # Platform identifiers
21
20
  PLATFORM_SLE = "SLE" # SystemLink Enterprise (cloud)
22
21
  PLATFORM_SLS = "SLS" # SystemLink Server (on-premises)
@@ -20,7 +20,6 @@ from rich.table import Table
20
20
  from rich.text import Text
21
21
  from rich.theme import Theme
22
22
 
23
-
24
23
  _THEME = Theme(
25
24
  {
26
25
  "brand": "bold cyan",
@@ -28,7 +28,6 @@ from .workspace_utils import (
28
28
  resolve_workspace_filter,
29
29
  )
30
30
 
31
-
32
31
  # ---------------------------------------------------------------------------
33
32
  # Helpers
34
33
  # ---------------------------------------------------------------------------
@@ -2797,6 +2797,7 @@ def register_system_commands(cli: Any) -> None:
2797
2797
  filter_expr,
2798
2798
  api_order_by,
2799
2799
  _parse_simple_response,
2800
+ take=take,
2800
2801
  )
2801
2802
  mock_resp: Any = FilteredResponse({"jobs": jobs})
2802
2803
  UniversalResponseHandler.handle_list_response(
@@ -26,7 +26,6 @@ from .utils import (
26
26
  )
27
27
  from .workspace_utils import get_workspace_display_name, resolve_workspace_id
28
28
 
29
-
30
29
  USER_QUERY_PAGE_SIZE = 100
31
30
  USER_JSON_DEFAULT_TAKE = 1000
32
31
  AUTH_WILDCARD_VALUES = {"*", "*/*", "*:*"}
@@ -17,6 +17,48 @@ import requests
17
17
  from .utils import ExitCodes, get_base_url, get_headers, get_ssl_verify
18
18
 
19
19
 
20
+ def _validated_proxy_origin(api_base: str) -> tuple[str, str]:
21
+ """Return a validated scheme and netloc for proxy requests.
22
+
23
+ Args:
24
+ api_base: Configured SystemLink base URL.
25
+
26
+ Returns:
27
+ Tuple of scheme and network location.
28
+
29
+ Raises:
30
+ ValueError: If the configured base URL is not a plain HTTP(S) origin.
31
+ """
32
+ parsed = urllib.parse.urlsplit(api_base)
33
+ if parsed.scheme not in {"http", "https"}:
34
+ raise ValueError("Editor proxy requires an HTTP(S) SystemLink base URL")
35
+ if not parsed.netloc or parsed.username or parsed.password:
36
+ raise ValueError("Editor proxy requires a base URL without embedded credentials")
37
+ if parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
38
+ raise ValueError("Editor proxy requires a base URL without path, query, or fragment")
39
+ return parsed.scheme, parsed.netloc
40
+
41
+
42
+ def _build_proxy_url(
43
+ origin_scheme: str,
44
+ origin_netloc: str,
45
+ target_path: str,
46
+ query: str = "",
47
+ ) -> str:
48
+ """Build a proxy URL from a validated origin and allowlisted path."""
49
+ return urllib.parse.urlunsplit((origin_scheme, origin_netloc, target_path, query, ""))
50
+
51
+
52
+ def _validated_proxy_path(request_path: str) -> str:
53
+ """Return a decoded absolute proxy path without dot-segments."""
54
+ decoded_path = urllib.parse.unquote(request_path)
55
+ if not decoded_path.startswith("/"):
56
+ raise ValueError("Editor proxy requires an absolute request path")
57
+ if any(segment in {".", ".."} for segment in decoded_path.split("/")):
58
+ raise ValueError("Editor proxy rejects paths containing dot-segments")
59
+ return decoded_path
60
+
61
+
20
62
  class DFFWebEditor:
21
63
  """Web-based editor for custom fields configurations."""
22
64
 
@@ -132,6 +174,7 @@ class DFFWebEditor:
132
174
  editor_dir = self._editor_dir # Capture for closure
133
175
  temp_path = self._temp_path # Capture for closure
134
176
  api_base = get_base_url().rstrip("/")
177
+ api_scheme, api_netloc = _validated_proxy_origin(api_base)
135
178
  default_headers = get_headers()
136
179
  ssl_verify = get_ssl_verify()
137
180
  secret = self._secret
@@ -149,9 +192,14 @@ class DFFWebEditor:
149
192
 
150
193
  def _proxy_request(self, method: str) -> bool:
151
194
  parsed = urllib.parse.urlparse(self.path)
195
+ try:
196
+ request_path = _validated_proxy_path(parsed.path)
197
+ except ValueError as exc:
198
+ self.send_error(400, str(exc))
199
+ return True
152
200
 
153
201
  # Serve slcli-config.json from temp directory
154
- if parsed.path == "/slcli-config.json" and method == "GET":
202
+ if request_path == "/slcli-config.json" and method == "GET":
155
203
  config_file = temp_path / "slcli-config.json"
156
204
  if config_file.exists():
157
205
  self.send_response(200)
@@ -164,7 +212,7 @@ class DFFWebEditor:
164
212
  return True
165
213
 
166
214
  # Serve config.json (the DFF configuration) from temp directory
167
- if parsed.path == "/config.json" and method == "GET":
215
+ if request_path == "/config.json" and method == "GET":
168
216
  config_file = temp_path / "config.json"
169
217
  if config_file.exists():
170
218
  self.send_response(200)
@@ -182,12 +230,12 @@ class DFFWebEditor:
182
230
  "/api/dff/update-configurations": "/nidynamicformfields/v1/update-configurations",
183
231
  }
184
232
 
185
- if parsed.path in path_map:
186
- target_path = path_map[parsed.path]
187
- elif parsed.path.startswith("/nidynamicformfields/v1/"):
188
- target_path = parsed.path
189
- elif parsed.path.startswith("/niuser/v1/workspaces"):
190
- target_path = parsed.path
233
+ if request_path in path_map:
234
+ target_path = path_map[request_path]
235
+ elif request_path.startswith("/nidynamicformfields/v1/"):
236
+ target_path = request_path
237
+ elif request_path.startswith("/niuser/v1/workspaces"):
238
+ target_path = request_path
191
239
  else:
192
240
  return False
193
241
 
@@ -197,9 +245,12 @@ class DFFWebEditor:
197
245
  self.send_error(403, "Forbidden: Missing or invalid editor secret")
198
246
  return True
199
247
 
200
- target_url = f"{api_base}{target_path}"
201
- if parsed.query:
202
- target_url = f"{target_url}?{parsed.query}"
248
+ target_url = _build_proxy_url(
249
+ origin_scheme=api_scheme,
250
+ origin_netloc=api_netloc,
251
+ target_path=target_path,
252
+ query=parsed.query,
253
+ )
203
254
 
204
255
  headers = dict(default_headers)
205
256
  data = None
@@ -35,7 +35,6 @@ from .workspace_utils import (
35
35
  resolve_workspace_filter,
36
36
  )
37
37
 
38
-
39
38
  # ---------------------------------------------------------------------------
40
39
  # URL helpers
41
40
  # ---------------------------------------------------------------------------
File without changes