dt-extensions-sdk 1.7.3__py3-none-any.whl → 1.8.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.
@@ -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
- dynatrace_extension/__about__.py,sha256=-BvmT9sstUN3n3QmUgym0fwfILxaCrccJsuxxXpn5H0,110
1
+ dynatrace_extension/__about__.py,sha256=feKH4_h_P2KAMzfeJfsy34a-VQCLlkBU5KBdHgyBGc4,110
2
2
  dynatrace_extension/__init__.py,sha256=MJNJYCFWLEwPmBLoETWFZddyUCMDgZfKkRycmmGM_w4,806
3
3
  dynatrace_extension/cli/__init__.py,sha256=HCboY_eJPoqjFmoPDsBL8Jk6aNvank8K7JpkVrgwzUM,123
4
- dynatrace_extension/cli/main.py,sha256=OTjJ4XHJvvYXj10a7WFFHVNnkyECPg1ClW6Os8piN8k,20168
4
+ dynatrace_extension/cli/main.py,sha256=vQrYd-k_82Dklo2ult-10nkM8IhFO4xRACyBCVWOnSE,22646
5
5
  dynatrace_extension/cli/schema.py,sha256=d8wKUodRiaU3hfSZDWVNpD15lBfhmif2oQ-k07IxcaA,3230
6
6
  dynatrace_extension/cli/create/__init__.py,sha256=NfyOJCVlxs8dYtfDAMHS1Q5SJTuZcFzOg5rtaI-ZPRE,72
7
7
  dynatrace_extension/cli/create/create.py,sha256=IcgyVcjud1QTUE99KNHVNTH7G0qQsQyldVchp6CG3v8,2695
@@ -17,10 +17,10 @@ dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.tem
17
17
  dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=cS79GVxJB-V-gocu4ZOjmZ54HXJNg89eXdLf89zDHJQ,1249
18
18
  dynatrace_extension/sdk/__init__.py,sha256=RsqQ1heGyCmSK3fhuEKAcxQIRCg4gEK0-eSkIehL5Nc,86
19
19
  dynatrace_extension/sdk/activation.py,sha256=KIoPWMZs3tKiMG8XhCfeNgRlz2vxDKcAASgSACcEfIQ,1456
20
- dynatrace_extension/sdk/callback.py,sha256=NW63fQBh-BeN-zfAeDY94wpSBoBHRsR3VLJCvxJPiaw,6703
20
+ dynatrace_extension/sdk/callback.py,sha256=S_ELy2D9Y2YhUsL2lBry3UZfmy3BrZgZM2hvKgQIQss,6949
21
21
  dynatrace_extension/sdk/communication.py,sha256=uTSURmgSHit2N1hHUc3-yKmEBVMHi6hDBrdb1EaCAsE,18419
22
22
  dynatrace_extension/sdk/event.py,sha256=J261imbFKpxfuAQ6Nfu3RRcsIQKKivy6fme1nww2g-8,388
23
- dynatrace_extension/sdk/extension.py,sha256=KZYVn5VDW19pfnqe6UnpzRVPYUasW8n8AYSvUYvvOUw,49723
23
+ dynatrace_extension/sdk/extension.py,sha256=ZbuoHQ-sbj4jUSz0w1Iz6IePrnC9a0BEwoXIdLKJsU8,49957
24
24
  dynatrace_extension/sdk/helper.py,sha256=m4gGHtIKYkfANC2MOGdxKUZlmH5tnZO6WTNqll27lyY,6476
25
25
  dynatrace_extension/sdk/metric.py,sha256=-kq7JWpk7UGvcjqafTt-o6k4urwhsGVXmnuQg7Sf9PQ,3622
26
26
  dynatrace_extension/sdk/runtime.py,sha256=7bC4gUJsVSHuL_E7r2EWrne95nm1BjZiMGkyNqA7ZCU,2796
@@ -31,8 +31,8 @@ dynatrace_extension/sdk/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
31
31
  dynatrace_extension/sdk/vendor/mureq/LICENSE,sha256=8AVcgZgiT_mvK1fOofXtRRr2f1dRXS_K21NuxQgP4VM,671
32
32
  dynatrace_extension/sdk/vendor/mureq/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  dynatrace_extension/sdk/vendor/mureq/mureq.py,sha256=znF4mvzk5L03CLNozRz8UpK-fMijmSkObDFwlbhwLUg,14656
34
- dt_extensions_sdk-1.7.3.dist-info/METADATA,sha256=irv3Te4njH0dWl4Yl-KyNN--ErS4PP7-xrghjwbtoqo,2721
35
- dt_extensions_sdk-1.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- dt_extensions_sdk-1.7.3.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
37
- dt_extensions_sdk-1.7.3.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
38
- dt_extensions_sdk-1.7.3.dist-info/RECORD,,
34
+ dt_extensions_sdk-1.8.0.dist-info/METADATA,sha256=gg6h4u5Vd29Z7GHQ6wTaSDCh5cuky1NLsvGTyeP8rg0,2801
35
+ dt_extensions_sdk-1.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ dt_extensions_sdk-1.8.0.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
37
+ dt_extensions_sdk-1.8.0.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
38
+ dt_extensions_sdk-1.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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: