dt-extensions-sdk 1.7.3__tar.gz → 1.8.0__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 (89) hide show
  1. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/PKG-INFO +6 -5
  2. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/README.md +2 -2
  3. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/guides/building.rst +13 -3
  4. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/guides/installation.rst +1 -1
  5. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/guides/migration.rst +1 -1
  6. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/__about__.py +1 -1
  7. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/main.py +73 -15
  8. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/callback.py +28 -18
  9. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/extension.py +6 -1
  10. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/pyproject.toml +6 -5
  11. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/cli/test_dt_sdk.py +2 -0
  12. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_extension.py +46 -1
  13. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/.github/workflows/build-package.yml +0 -0
  14. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/.github/workflows/gh-pages-docs.yml +0 -0
  15. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/.github/workflows/publish.yml +0 -0
  16. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/.gitignore +0 -0
  17. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/LICENSE.txt +0 -0
  18. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/dt-sdk-header.png +0 -0
  19. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/dt-sdk-logo.png +0 -0
  20. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/favicon.ico +0 -0
  21. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/img/migrate-01-new-extension.png +0 -0
  22. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/img/migrate-02-type.png +0 -0
  23. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/img/migrate-03-import.png +0 -0
  24. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/img/migrate-04-import-remote.png +0 -0
  25. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/img/migrate-05-activation.png +0 -0
  26. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/_static/img/migrate-06-activation-config.png +0 -0
  27. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/events/event_severity.rst +0 -0
  28. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/events/event_type.rst +0 -0
  29. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/events/index.rst +0 -0
  30. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/extension.rst +0 -0
  31. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/metrics/index.rst +0 -0
  32. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/metrics/metric.rst +0 -0
  33. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/api/metrics/metric_type.rst +0 -0
  34. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/assemble.rst +0 -0
  35. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/build.rst +0 -0
  36. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/create.rst +0 -0
  37. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/gencerts.rst +0 -0
  38. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/help.rst +0 -0
  39. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/run.rst +0 -0
  40. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/sign.rst +0 -0
  41. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/upload.rst +0 -0
  42. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/cli/wheel.rst +0 -0
  43. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/conf.py +0 -0
  44. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/guides/extension_structure.rst +0 -0
  45. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/index.rst +0 -0
  46. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/docs/requirements.txt +0 -0
  47. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/__init__.py +0 -0
  48. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/__init__.py +0 -0
  49. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/__init__.py +0 -0
  50. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/create.py +0 -0
  51. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/.gitignore.template +0 -0
  52. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/README.md.template +0 -0
  53. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/activation.json.template +0 -0
  54. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template +0 -0
  55. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template +0 -0
  56. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.template +0 -0
  57. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template +0 -0
  58. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/ruff.toml.template +0 -0
  59. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/secrets.json.template +0 -0
  60. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/create/extension_template/setup.py.template +0 -0
  61. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/cli/schema.py +0 -0
  62. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/__init__.py +0 -0
  63. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/activation.py +0 -0
  64. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/communication.py +0 -0
  65. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/event.py +0 -0
  66. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/helper.py +0 -0
  67. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/metric.py +0 -0
  68. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/runtime.py +0 -0
  69. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/snapshot.py +0 -0
  70. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/status.py +0 -0
  71. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/throttled_logger.py +0 -0
  72. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/vendor/__init__.py +0 -0
  73. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/vendor/mureq/LICENSE +0 -0
  74. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/vendor/mureq/__init__.py +0 -0
  75. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/dynatrace_extension/sdk/vendor/mureq/mureq.py +0 -0
  76. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/__init__.py +0 -0
  77. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/cli/__init__.py +0 -0
  78. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/cli/test_templates.py +0 -0
  79. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/cli/test_types.py +0 -0
  80. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/data/snapshot.json +0 -0
  81. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/__init__.py +0 -0
  82. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_activation.py +0 -0
  83. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_callback.py +0 -0
  84. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_communication.py +0 -0
  85. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_endpoints_sfm.py +0 -0
  86. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_metric.py +0 -0
  87. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_runtime_properties.py +0 -0
  88. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_snapshot.py +0 -0
  89. {dt_extensions_sdk-1.7.3 → dt_extensions_sdk-1.8.0}/tests/sdk/test_status.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dt-extensions-sdk
3
- Version: 1.7.3
3
+ Version: 1.8.0
4
4
  Project-URL: Documentation, https://github.com/dynatrace-extensions/dt-extensions-python-sdk#readme
5
5
  Project-URL: Issues, https://github.com/dynatrace-extensions/dt-extensions-python-sdk/issues
6
6
  Project-URL: Source, https://github.com/dynatrace-extensions/dt-extensions-python-sdk
@@ -10,20 +10,21 @@ License-File: LICENSE.txt
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.14
13
14
  Classifier: Programming Language :: Python :: Implementation :: CPython
14
15
  Classifier: Programming Language :: Python :: Implementation :: PyPy
15
- Requires-Python: <3.11,>=3.10
16
+ Requires-Python: <3.15,>=3.10
16
17
  Provides-Extra: cli
17
18
  Requires-Dist: dt-cli>=1.6.13; extra == 'cli'
18
19
  Requires-Dist: pyyaml; extra == 'cli'
19
20
  Requires-Dist: ruff; extra == 'cli'
20
- Requires-Dist: typer[all]; extra == 'cli'
21
+ Requires-Dist: typer; extra == 'cli'
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  # Dynatrace Extensions Python SDK
24
25
 
25
26
  [![PyPI - Version](https://img.shields.io/pypi/v/dt-extensions-sdk.svg)](https://pypi.org/project/dt-extensions-sdk)
26
- [![PyPI - Python Version](https://img.shields.io/badge/python-3.10-blue)](https://img.shields.io/badge/python-3.10-blue)
27
+ [![PyPI - Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.14-blue)](https://img.shields.io/badge/python-3.10%20%7C%203.14-blue)
27
28
 
28
29
  -----
29
30
 
@@ -41,7 +42,7 @@ The documentation can be found on [github pages](https://dynatrace-extensions.gi
41
42
 
42
43
  ### Requirements:
43
44
 
44
- * Python 3.10
45
+ * Python 3.10 or 3.14
45
46
 
46
47
  ### Install the SDK
47
48
 
@@ -1,7 +1,7 @@
1
1
  # Dynatrace Extensions Python SDK
2
2
 
3
3
  [![PyPI - Version](https://img.shields.io/pypi/v/dt-extensions-sdk.svg)](https://pypi.org/project/dt-extensions-sdk)
4
- [![PyPI - Python Version](https://img.shields.io/badge/python-3.10-blue)](https://img.shields.io/badge/python-3.10-blue)
4
+ [![PyPI - Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.14-blue)](https://img.shields.io/badge/python-3.10%20%7C%203.14-blue)
5
5
 
6
6
  -----
7
7
 
@@ -19,7 +19,7 @@ The documentation can be found on [github pages](https://dynatrace-extensions.gi
19
19
 
20
20
  ### Requirements:
21
21
 
22
- * Python 3.10
22
+ * Python 3.10 or 3.14
23
23
 
24
24
  ### Install the SDK
25
25
 
@@ -27,10 +27,12 @@ Native dependencies
27
27
  | Your extension will run on a Dynatrace **Activegate** or **OneAgent**, which is a Linux or Windows machine, and it has a specific version of python.
28
28
  | This means that your extension must be built on a machine that has the same version of python as the Activegate.
29
29
  |
30
- | At this time, Dynatrace extensions run on **python 3.10**.
30
+ | Dynatrace extensions support **python 3.10** and **python 3.14**.
31
+ | We plan to end support for **python 3.10** around October/2026.
31
32
  |
32
- | When you build the extension with **dt-sdk build**, it downloads the dependencies **whl** files and places them in the lib folder of the extension.
33
+ | When you build the extension with **dt-sdk build**, it downloads the dependencies **whl** files and places them in the lib folder of the extension.
33
34
  | To obtain whl files for different a operating system than what the build machine is, the SDK provides the **--extra-platform** flag.
35
+ | To download wheels for multiple Python versions, use the **--python-version** flag (can be specified multiple times).
34
36
  |
35
37
  | In summary, when building from Windows, you should use:
36
38
  |
@@ -43,6 +45,14 @@ Native dependencies
43
45
  | To get the correct extra wheel files for linux. Note, **manylinux2014_x86_64** works for several packages, but not all of them.
44
46
  | You need to investigate the dependencies of your extension to find the correct extra platform if that is the case.
45
47
  |
48
+
49
+ | To build for multiple Python versions:
50
+ |
51
+
52
+ .. code-block:: bash
53
+
54
+ dt-sdk build --python-version 3.10 --python-version 3.14
55
+
46
56
  | When building from Linux, you should use:
47
57
  |
48
58
 
@@ -120,7 +130,7 @@ Musl vs libc
120
130
  | Extensions run on `libc`_ based systems, like Ubuntu, CentOS, Windows, etc.
121
131
  | You should not use a `musl`_ based system, like Alpine, to build extensions.
122
132
  |
123
- | This means that if you are using a docker container to build the extension, you should use the **python:3.10** image, or any other image that is based on a `libc`_ system.
133
+ | This means that if you are using a docker container to build the extension, you should use a **python:3.10** or **python:3.14** image, or any other image that is based on a `libc`_ system.
124
134
  |
125
135
  | The reason for this is that a **musl** based system will download native whl files that are not compatible with **libc** based systems.
126
136
 
@@ -9,7 +9,7 @@ Requirements
9
9
  ``dt-extensions-sdk`` requires only the following dependencies to be
10
10
  present in the environment.
11
11
 
12
- - Python 3.10
12
+ - Python 3.10 or 3.14
13
13
 
14
14
  Installing from PyPI
15
15
  ^^^^^^^^^^^^^^^^^^^^
@@ -6,7 +6,7 @@ Migrating EF1 Extensions
6
6
  Requirements
7
7
  ============
8
8
 
9
- * `Python 3.10 <https://www.python.org/downloads/>`_
9
+ * `Python 3.10 or 3.14 <https://www.python.org/downloads/>`_
10
10
  * ``dt-extensions-sdk`` installed and in your ``PATH`` (you can run with ``dt-sdk --help``).
11
11
  * `VSCode <https://code.visualstudio.com>`_ with the `Dynatrace Extensions <https://marketplace.visualstudio.com/items?itemName=DynatracePlatformExtensions.dynatrace-extensions>`_ extension installed.
12
12
 
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
 
6
- __version__ = "1.7.3"
6
+ __version__ = "1.8.0"
@@ -17,10 +17,8 @@ from .schema import ExtensionYaml
17
17
  app = typer.Typer(pretty_exceptions_show_locals=False, pretty_exceptions_enable=False)
18
18
  console = Console()
19
19
 
20
- # if we are not python 3.10.X, exit with an error
21
- if sys.version_info < (3, 10) or sys.version_info >= (3, 11):
22
- console.print(f"Python 3.10.X is required to build extensions, you are using {sys.version_info}", style="bold red")
23
- sys.exit(1)
20
+ SUPPORTED_PYTHON_VERSIONS = ["3.10", "3.14"]
21
+
24
22
 
25
23
  CERT_DIR_ENVIRONMENT_VAR = "DT_CERTIFICATES_FOLDER"
26
24
  CERTIFICATE_DEFAULT_PATH = Path.home() / ".dynatrace" / "certificates"
@@ -106,6 +104,12 @@ def build(
106
104
  "-o",
107
105
  help="Only build for the extra platforms, useful when building from arm64 (mac)",
108
106
  ),
107
+ python_versions: list[str] | None = typer.Option(
108
+ None,
109
+ "--python-version",
110
+ "-p",
111
+ help=f"Python versions to download wheels for. Supported: {', '.join(SUPPORTED_PYTHON_VERSIONS)}",
112
+ ),
109
113
  ):
110
114
  """
111
115
  Builds and signs an extension using the developer fused key-certificate
@@ -119,6 +123,7 @@ def build(
119
123
  :param extra_index_url: Extra index url to use when downloading dependencies
120
124
  :param find_links: Extra index url to use when downloading dependencies
121
125
  :param only_extra_platforms: If true, only build for the extra platforms, useful when building from arm64
126
+ :param python_versions: Python versions to download wheels for, defaults to ['3.10']
122
127
  """
123
128
  console.print(f"Building and signing extension from {extension_dir} to {target_directory}", style="cyan")
124
129
  if target_directory is None:
@@ -127,7 +132,7 @@ def build(
127
132
  target_directory.mkdir()
128
133
 
129
134
  console.print("Stage 1 - Download and build dependencies", style="bold blue")
130
- wheel(extension_dir, extra_platforms, extra_index_url, find_links, only_extra_platforms)
135
+ wheel(extension_dir, extra_platforms, extra_index_url, find_links, only_extra_platforms, python_versions)
131
136
 
132
137
  console.print("Stage 2 - Create the extension zip file", style="bold blue")
133
138
  built_zip = assemble(extension_dir, target_directory)
@@ -184,6 +189,11 @@ def assemble(
184
189
  return output
185
190
 
186
191
 
192
+ def _version_to_pip_version(version: str) -> str:
193
+ """Convert a version string like '3.10' to pip format '310'."""
194
+ return version.replace(".", "")
195
+
196
+
187
197
  @app.command(help="Downloads the dependencies of the extension to the lib folder")
188
198
  def wheel(
189
199
  extension_dir: Path = typer.Argument(".", help="Path to the python extension"),
@@ -202,6 +212,12 @@ def wheel(
202
212
  "-o",
203
213
  help="Only build for the extra platforms, useful when building from arm64 (mac)",
204
214
  ),
215
+ python_versions: list[str] | None = typer.Option(
216
+ None,
217
+ "--python-version",
218
+ "-p",
219
+ help=f"Python versions to download wheels for. Supported: {', '.join(SUPPORTED_PYTHON_VERSIONS)}",
220
+ ),
205
221
  ):
206
222
  """
207
223
  Builds the extension and it's dependencies into wheel files
@@ -212,27 +228,43 @@ def wheel(
212
228
  :param extra_index_url: Extra index url to use when downloading dependencies
213
229
  :param find_links: Extra index url to use when downloading dependencies
214
230
  :param only_extra_platforms: If true, only build for the extra platforms, useful when building from arm64
231
+ :param python_versions: Python versions to download wheels for, defaults to ['3.10']
215
232
  """
233
+ # Handle OptionInfo objects when called directly (not via CLI)
234
+ if python_versions is None or isinstance(python_versions, typer.models.OptionInfo):
235
+ python_versions = ["3.10"]
236
+ if only_extra_platforms is None or isinstance(only_extra_platforms, typer.models.OptionInfo):
237
+ only_extra_platforms = False
238
+
239
+ # Validate python versions
240
+ for version in python_versions:
241
+ if version not in SUPPORTED_PYTHON_VERSIONS:
242
+ msg = (
243
+ f"Python version {version} is not supported. Supported versions: {', '.join(SUPPORTED_PYTHON_VERSIONS)}"
244
+ )
245
+ console.print(msg, style="bold red")
246
+ raise typer.Exit(1)
247
+
216
248
  relative_lib_folder_dir = "extension/lib"
217
249
  lib_folder: Path = extension_dir / relative_lib_folder_dir
218
250
  _clean_directory(lib_folder)
219
251
 
220
252
  console.print(f"Downloading dependencies to {lib_folder}", style="cyan")
221
253
 
222
- # Downloads the dependencies and places them in the lib folder
223
- command = [sys.executable, "-m", "pip", "wheel", "-w", relative_lib_folder_dir]
254
+ # Build the wheel for the extension itself (no deps)
255
+ command = [sys.executable, "-m", "pip", "wheel", "-w", relative_lib_folder_dir, "--no-deps"]
224
256
  if extra_index_url is not None:
225
257
  command.extend(["--extra-index-url", extra_index_url])
226
258
  if find_links is not None:
227
259
  command.extend(["--find-links", find_links])
228
- if only_extra_platforms:
229
- command.append("--no-deps")
230
260
  command.append(".")
231
261
  run_process(command, cwd=extension_dir)
232
262
 
233
- if extra_platforms:
234
- for extra_platform in extra_platforms:
235
- console.print(f"Downloading wheels for platform {extra_platform}", style="cyan")
263
+ # Download dependencies for the current platform for each requested python version
264
+ if not only_extra_platforms:
265
+ for version in python_versions:
266
+ pip_version = _version_to_pip_version(version)
267
+ console.print(f"Downloading wheels for Python {version} (current platform)", style="cyan")
236
268
  command = [
237
269
  sys.executable,
238
270
  "-m",
@@ -241,17 +273,43 @@ def wheel(
241
273
  "-d",
242
274
  relative_lib_folder_dir,
243
275
  "--only-binary=:all:",
244
- "--platform",
245
- extra_platform,
276
+ "--python-version",
277
+ pip_version,
246
278
  ]
247
279
  if extra_index_url:
248
280
  command.extend(["--extra-index-url", extra_index_url])
249
281
  if find_links:
250
282
  command.extend(["--find-links", find_links])
251
283
  command.append(".")
252
-
253
284
  run_process(command, cwd=extension_dir)
254
285
 
286
+ # Download dependencies for extra platforms for each requested python version
287
+ if extra_platforms:
288
+ for extra_platform in extra_platforms:
289
+ for version in python_versions:
290
+ pip_version = _version_to_pip_version(version)
291
+ console.print(f"Downloading wheels for Python {version}, platform {extra_platform}", style="cyan")
292
+ command = [
293
+ sys.executable,
294
+ "-m",
295
+ "pip",
296
+ "download",
297
+ "-d",
298
+ relative_lib_folder_dir,
299
+ "--only-binary=:all:",
300
+ "--python-version",
301
+ pip_version,
302
+ "--platform",
303
+ extra_platform,
304
+ ]
305
+ if extra_index_url:
306
+ command.extend(["--extra-index-url", extra_index_url])
307
+ if find_links:
308
+ command.extend(["--find-links", find_links])
309
+ command.append(".")
310
+
311
+ run_process(command, cwd=extension_dir)
312
+
255
313
  console.print(f"Installed dependencies to {lib_folder}", style="bold green")
256
314
 
257
315
 
@@ -23,6 +23,7 @@ class WrappedCallback:
23
23
  kwargs: dict | None = None,
24
24
  running_in_sim=False,
25
25
  activation_type: ActivationType | None = None,
26
+ offset_seconds: float | None = None,
26
27
  ):
27
28
  self.callback: Callable = callback
28
29
  if args is None:
@@ -48,6 +49,7 @@ class WrappedCallback:
48
49
  self.timeouts_count = 0 # counter per interval = 1 min by default
49
50
  self.exception_count = 0 # counter per interval = 1 min by default
50
51
  self.iterations = 0 # how many times we ran the callback iterator for this callback
52
+ self.offset_seconds = offset_seconds or self.calculate_initial_wait_time()
51
53
 
52
54
  def get_current_time_with_cluster_diff(self):
53
55
  return datetime.now() + timedelta(milliseconds=self.cluster_time_diff)
@@ -97,24 +99,28 @@ class WrappedCallback:
97
99
  def name(self):
98
100
  return self.callback.__name__
99
101
 
102
+ def calculate_initial_wait_time(self) -> float:
103
+ """
104
+ Here we chose a random second between 5 and 55 to start the callback
105
+ This is to distribute load for extension running on this host
106
+ """
107
+
108
+ now = self.get_current_time_with_cluster_diff()
109
+ random_second = random.randint(5, 55) # noqa: S311
110
+ next_execution = datetime.now().replace(second=random_second, microsecond=0)
111
+ if next_execution <= now:
112
+ # The random chosen second already passed this minute
113
+ next_execution += timedelta(minutes=1)
114
+ wait_time = (next_execution - now).total_seconds()
115
+ self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
116
+ return wait_time
117
+
100
118
  def initial_wait_time(self) -> float:
101
- if not self.running_in_sim:
102
- """
103
- Here we chose a random second between 5 and 55 to start the callback
104
- This is to distribute load for extension running on this host
105
- When running from the simulator, this is not done
106
- """
107
-
108
- now = self.get_current_time_with_cluster_diff()
109
- random_second = random.randint(5, 55) # noqa: S311
110
- next_execution = datetime.now().replace(second=random_second, microsecond=0)
111
- if next_execution <= now:
112
- # The random chosen second already passed this minute
113
- next_execution += timedelta(minutes=1)
114
- wait_time = (next_execution - now).total_seconds()
115
- self.logger.debug(f"Randomly choosing next execution time for callback {self} to be {next_execution}")
116
- return wait_time
117
- return 0
119
+ # When running from the simulator, we don't want any offset
120
+ if self.running_in_sim:
121
+ return 0
122
+
123
+ return self.offset_seconds
118
124
 
119
125
  def get_adjusted_metric_timestamp(self) -> datetime:
120
126
  """
@@ -151,4 +157,8 @@ class WrappedCallback:
151
157
  This is done using execution total, the interval and the start timestamp
152
158
  :return: float
153
159
  """
154
- return self.start_timestamp_monotonic + self.interval.total_seconds() * (self.iterations or 1)
160
+ return (
161
+ self.initial_wait_time()
162
+ + self.start_timestamp_monotonic
163
+ + self.interval.total_seconds() * (self.iterations or 1)
164
+ )
@@ -385,6 +385,7 @@ class Extension:
385
385
  interval: timedelta | int,
386
386
  args: tuple | None = None,
387
387
  activation_type: ActivationType | None = None,
388
+ offset_seconds: float | None = None,
388
389
  ) -> None:
389
390
  """Schedule a method to be executed periodically.
390
391
 
@@ -398,6 +399,7 @@ class Extension:
398
399
  args: Arguments to the callback, if any
399
400
  activation_type: Optional activation type when this callback should run,
400
401
  can be 'ActivationType.LOCAL' or 'ActivationType.REMOTE'
402
+ offset_seconds: Optional offset of first execution represented in seconds. Offset is random if `offset_seconds` is `None`.
401
403
  """
402
404
 
403
405
  if isinstance(interval, int):
@@ -407,7 +409,10 @@ class Extension:
407
409
  msg = f"Interval must be at least 1 second, got {interval.total_seconds()} seconds"
408
410
  raise ValueError(msg)
409
411
 
410
- callback = WrappedCallback(interval, callback, api_logger, args, activation_type=activation_type)
412
+ callback = WrappedCallback(
413
+ interval, callback, api_logger, args, activation_type=activation_type, offset_seconds=offset_seconds
414
+ )
415
+
411
416
  if self._is_fastcheck:
412
417
  self._scheduled_callbacks_before_run.append(callback)
413
418
  else:
@@ -7,7 +7,7 @@ name = "dt-extensions-sdk"
7
7
  dynamic = ["version"]
8
8
  description = ''
9
9
  readme = "README.md"
10
- requires-python = ">=3.10,<3.11"
10
+ requires-python = ">=3.10,<3.15"
11
11
  license = "MIT"
12
12
  keywords = []
13
13
  authors = [
@@ -17,13 +17,14 @@ classifiers = [
17
17
  "Development Status :: 4 - Beta",
18
18
  "Programming Language :: Python",
19
19
  "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.14",
20
21
  "Programming Language :: Python :: Implementation :: CPython",
21
22
  "Programming Language :: Python :: Implementation :: PyPy",
22
23
  ]
23
24
  dependencies = []
24
25
 
25
26
  [project.optional-dependencies]
26
- cli = [ "dt-cli>=1.6.13", "typer[all]", "pyyaml", "ruff"]
27
+ cli = [ "dt-cli>=1.6.13", "typer", "pyyaml", "ruff"]
27
28
 
28
29
  [project.urls]
29
30
  Documentation = "https://github.com/dynatrace-extensions/dt-extensions-python-sdk#readme"
@@ -45,7 +46,7 @@ packages = ["dynatrace_extension"]
45
46
  dependencies = [
46
47
  "coverage[toml]>=6.5",
47
48
  "pytest",
48
- "typer[all]",
49
+ "typer",
49
50
  "pyyaml",
50
51
  "dt-cli>=1.6.13",
51
52
  "freezegun"
@@ -72,7 +73,7 @@ dependencies = [
72
73
  "black>=23.1.0",
73
74
  "mypy>=1.0.0",
74
75
  "ruff>=0.9.10",
75
- "typer[all]",
76
+ "typer",
76
77
  "pyyaml",
77
78
  "pytest",
78
79
  "freezegun"
@@ -101,7 +102,7 @@ dependencies = [
101
102
  "sphinxcontrib-programoutput",
102
103
  "wutch",
103
104
  "dt-cli",
104
- "typer[all]",
105
+ "typer",
105
106
  "pyyaml",
106
107
  "tomli"
107
108
  ]
@@ -98,6 +98,8 @@ class TestDtSdk(TestCase):
98
98
  extra_platforms=None,
99
99
  extra_index_url=None,
100
100
  find_links=None,
101
+ only_extra_platforms=False,
102
+ python_versions=None,
101
103
  )
102
104
 
103
105
  # Check that the built extension file exists
@@ -1,7 +1,7 @@
1
1
  import threading
2
2
  import time
3
3
  import unittest
4
- from datetime import timedelta
4
+ from datetime import datetime, timedelta
5
5
  from unittest.mock import MagicMock, mock_open, patch
6
6
 
7
7
  import pytest
@@ -277,6 +277,51 @@ class TestExtension(unittest.TestCase):
277
277
  callback_wait.wait(timeout=5)
278
278
  self.assertEqual(callback_call_count, 2)
279
279
 
280
+ def test_schedule_callback_with_offset(self):
281
+ extension = Extension()
282
+ extension.logger = MagicMock()
283
+ extension._running_in_sim = False
284
+ extension._is_fastcheck = False
285
+ extension._client = MagicMock()
286
+
287
+ callback_call_dt = {"callback_random": [], "callback_offset_15": [], "callback_offset_45": []}
288
+
289
+ def callback_random():
290
+ nonlocal callback_call_dt
291
+ callback_call_dt["callback_random"].append(datetime.now())
292
+
293
+ def callback_offset_15():
294
+ nonlocal callback_call_dt
295
+ callback_call_dt["callback_offset_15"].append(datetime.now())
296
+
297
+ def callback_offset_45():
298
+ nonlocal callback_call_dt
299
+ callback_call_dt["callback_offset_45"].append(datetime.now())
300
+
301
+ extension.schedule(callback_random, timedelta(seconds=60)) # Random offset in range 5 - 55 second of the minute
302
+ extension.schedule(callback_offset_15, timedelta(seconds=60), offset_seconds=15) # Offset from now
303
+ extension.schedule(callback_offset_45, timedelta(seconds=60), offset_seconds=45) # Offset from now
304
+
305
+ dt_start = datetime.now()
306
+ while datetime.now() - dt_start < timedelta(seconds=125):
307
+ extension._scheduler.run(blocking=False)
308
+ time.sleep(0.1)
309
+
310
+ assert len(callback_call_dt["callback_random"]) >= 2
311
+ assert len(callback_call_dt["callback_offset_15"]) >= 2
312
+ assert len(callback_call_dt["callback_offset_45"]) >= 2
313
+
314
+ assert 4 <= callback_call_dt["callback_random"][0].second <= 56
315
+ assert 14 <= (callback_call_dt["callback_offset_15"][0] - dt_start).total_seconds() <= 16
316
+ assert 44 <= (callback_call_dt["callback_offset_45"][0] - dt_start).total_seconds() <= 46
317
+
318
+ def interval(x):
319
+ return (x[1] - x[0]).total_seconds()
320
+
321
+ assert 59 <= interval(callback_call_dt["callback_random"]) <= 61
322
+ assert 59 <= interval(callback_call_dt["callback_offset_15"]) <= 61
323
+ assert 59 <= interval(callback_call_dt["callback_offset_45"]) <= 61
324
+
280
325
  def test_schedule_method_decorator(self):
281
326
  class MyExt(Extension):
282
327
  def __init__(self) -> None: