dt-extensions-sdk 1.4.0__py3-none-any.whl → 1.5.1__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.4.0
3
+ Version: 1.5.1
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
@@ -16,6 +16,7 @@ Requires-Python: <3.11,>=3.10
16
16
  Provides-Extra: cli
17
17
  Requires-Dist: dt-cli>=1.6.13; extra == 'cli'
18
18
  Requires-Dist: pyyaml; extra == 'cli'
19
+ Requires-Dist: ruff; extra == 'cli'
19
20
  Requires-Dist: typer[all]; extra == 'cli'
20
21
  Description-Content-Type: text/markdown
21
22
 
@@ -1,35 +1,36 @@
1
- dynatrace_extension/__about__.py,sha256=0eF7oFPAs4l6aF4bObZHJnLcIaLQ07bZ0ozH7nzz4HY,110
1
+ dynatrace_extension/__about__.py,sha256=5K7C4flDpJDmdHw8pNksZ4c9GLxQrwNkYMwLMvckJ-I,110
2
2
  dynatrace_extension/__init__.py,sha256=BvQuknmA7ti3WJi3zEXZfY7aAxJrie37VNitWICsUvI,752
3
3
  dynatrace_extension/cli/__init__.py,sha256=HCboY_eJPoqjFmoPDsBL8Jk6aNvank8K7JpkVrgwzUM,123
4
- dynatrace_extension/cli/main.py,sha256=Z8gFcp0vIMBkzV6jUd5mwvP1U0JcLqAHoMLJw_6puz4,18260
4
+ dynatrace_extension/cli/main.py,sha256=OTjJ4XHJvvYXj10a7WFFHVNnkyECPg1ClW6Os8piN8k,20168
5
5
  dynatrace_extension/cli/schema.py,sha256=d8wKUodRiaU3hfSZDWVNpD15lBfhmif2oQ-k07IxcaA,3230
6
6
  dynatrace_extension/cli/create/__init__.py,sha256=NfyOJCVlxs8dYtfDAMHS1Q5SJTuZcFzOg5rtaI-ZPRE,72
7
- dynatrace_extension/cli/create/create.py,sha256=apXden2M93MDDDm7aa-Os-AEtUtyKbk_PsS56j32NK4,2708
7
+ dynatrace_extension/cli/create/create.py,sha256=IcgyVcjud1QTUE99KNHVNTH7G0qQsQyldVchp6CG3v8,2695
8
8
  dynatrace_extension/cli/create/extension_template/.gitignore.template,sha256=FPye23W8dqmked4HQBCDCAKFf1UbBGkwhKjQpgmXhdg,3101
9
9
  dynatrace_extension/cli/create/extension_template/README.md.template,sha256=QcV0fYqJ1PHaouKdGQgzveJY5zAFBSICp7xSfgnoQj0,637
10
10
  dynatrace_extension/cli/create/extension_template/activation.json.template,sha256=qX-Fgq_JhUWNYMe1-RMcjwQPzF4scJYGfGlBr043UiY,266
11
+ dynatrace_extension/cli/create/extension_template/ruff.toml.template,sha256=knGDPYHrdrlSDXpe3XNofcgAP-6XyzyHNqLd6Tr8bbs,1853
11
12
  dynatrace_extension/cli/create/extension_template/secrets.json.template,sha256=fr-ya8lR0TiUmr6lf-PFko6RZw6sr54FxtMbqIWBVkA,36
12
- dynatrace_extension/cli/create/extension_template/setup.py.template,sha256=M4HPg3UFkvmWFHAz6vHbDG5Ocb-ZiR0TQuLt9eedW1M,846
13
+ dynatrace_extension/cli/create/extension_template/setup.py.template,sha256=CPSx_mRKP6R273wFN-tJFQEsMNF2ktSO-I58mAWLM9M,829
13
14
  dynatrace_extension/cli/create/extension_template/extension/activationSchema.json.template,sha256=me3DL_Q449q4VaOStTqaBL-gUkKnlafC8L2093O0LOY,2905
14
15
  dynatrace_extension/cli/create/extension_template/extension/extension.yaml.template,sha256=qDyAURGXKsZBIPjW2s1BPhD-xOC8_FoBgA33HbsOkAU,299
15
16
  dynatrace_extension/cli/create/extension_template/extension_name/__init__.py.template,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=NYuZ6BWZ6HOs4TCuboh6S0nuYp3F8MXW2lmjSpdREEA,1205
17
+ dynatrace_extension/cli/create/extension_template/extension_name/__main__.py.template,sha256=cS79GVxJB-V-gocu4ZOjmZ54HXJNg89eXdLf89zDHJQ,1249
17
18
  dynatrace_extension/sdk/__init__.py,sha256=RsqQ1heGyCmSK3fhuEKAcxQIRCg4gEK0-eSkIehL5Nc,86
18
- dynatrace_extension/sdk/activation.py,sha256=goTbT1tD2kn8xfyXFdTy_cTZNcFPJpgbvQM8HOzKECA,1480
19
- dynatrace_extension/sdk/callback.py,sha256=eMpC0F3fCI82mWHIFgmy9QmKl7Kq_9dSaanHdV6o7hA,6511
20
- dynatrace_extension/sdk/communication.py,sha256=QkJgEBblOen-jmvsb3ZfYZYglYUc7jHbkEgPtRj9l6w,19123
19
+ dynatrace_extension/sdk/activation.py,sha256=KIoPWMZs3tKiMG8XhCfeNgRlz2vxDKcAASgSACcEfIQ,1456
20
+ dynatrace_extension/sdk/callback.py,sha256=--GyC5aDAhgRix8QLHHvp7KjYMIECTecy9jJWX0wyj8,6488
21
+ dynatrace_extension/sdk/communication.py,sha256=_u3VdftaI8N59Qxjtn9H0pJetWMhhrC6fNLPEixHmFw,19142
21
22
  dynatrace_extension/sdk/event.py,sha256=J261imbFKpxfuAQ6Nfu3RRcsIQKKivy6fme1nww2g-8,388
22
- dynatrace_extension/sdk/extension.py,sha256=59hg3aUXbrkhvEB7za8jujv1LZQ4sSx4ot9scijadQU,44926
23
- dynatrace_extension/sdk/helper.py,sha256=ZNrO9ao2hE3KQ934vAYD74k0fCr6QTG-_bAvbk9-hi8,6562
24
- dynatrace_extension/sdk/metric.py,sha256=7VClzJCFJNDCxA-d69uTu1pdPtDZBTwq7fbafs_L6nQ,3690
25
- dynatrace_extension/sdk/runtime.py,sha256=jyYsM1x-gMnW68eWq8IoZZZBarHgIcr_nVeGDDgpRDk,2802
26
- dynatrace_extension/sdk/snapshot.py,sha256=0Zx4lmhanoLvfvMTnkxWfcIsx2VUDm99FAqV0XZHoLA,7100
23
+ dynatrace_extension/sdk/extension.py,sha256=2mFDECsqZziYLGuxY3sYoVyWfWlhgc0ZUYwuUiXJQaA,45192
24
+ dynatrace_extension/sdk/helper.py,sha256=m4gGHtIKYkfANC2MOGdxKUZlmH5tnZO6WTNqll27lyY,6476
25
+ dynatrace_extension/sdk/metric.py,sha256=-kq7JWpk7UGvcjqafTt-o6k4urwhsGVXmnuQg7Sf9PQ,3622
26
+ dynatrace_extension/sdk/runtime.py,sha256=7bC4gUJsVSHuL_E7r2EWrne95nm1BjZiMGkyNqA7ZCU,2796
27
+ dynatrace_extension/sdk/snapshot.py,sha256=LnWVCtCK4NIEV3_kX-ly_LGHpNBSeErtsxCI1PH3L28,7521
27
28
  dynatrace_extension/sdk/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  dynatrace_extension/sdk/vendor/mureq/LICENSE,sha256=8AVcgZgiT_mvK1fOofXtRRr2f1dRXS_K21NuxQgP4VM,671
29
30
  dynatrace_extension/sdk/vendor/mureq/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  dynatrace_extension/sdk/vendor/mureq/mureq.py,sha256=znF4mvzk5L03CLNozRz8UpK-fMijmSkObDFwlbhwLUg,14656
31
- dt_extensions_sdk-1.4.0.dist-info/METADATA,sha256=LYTXYtBm33nrBL_jz22oNjpB0mRIhIdr27pFuHgNzao,2685
32
- dt_extensions_sdk-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- dt_extensions_sdk-1.4.0.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
34
- dt_extensions_sdk-1.4.0.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
35
- dt_extensions_sdk-1.4.0.dist-info/RECORD,,
32
+ dt_extensions_sdk-1.5.1.dist-info/METADATA,sha256=ItVzENof2_FYkmDls53v9pCU8hexGrmaQDRkPc2BBuE,2721
33
+ dt_extensions_sdk-1.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ dt_extensions_sdk-1.5.1.dist-info/entry_points.txt,sha256=pweyOCgENGHjOlT6_kXYaBPOrE3p18K0UettqnNlnoE,55
35
+ dt_extensions_sdk-1.5.1.dist-info/licenses/LICENSE.txt,sha256=3Zihv0lOVYHNfDkJC-tUAU6euP9r2NexsDW4w-zqgVk,1078
36
+ dt_extensions_sdk-1.5.1.dist-info/RECORD,,
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
 
6
- __version__ = "1.4.0"
6
+ __version__ = "1.5.1"
@@ -2,7 +2,7 @@ import os
2
2
  import re
3
3
  import shutil
4
4
  from pathlib import Path
5
- from typing import Final, List, NamedTuple, Tuple
5
+ from typing import Final, NamedTuple
6
6
 
7
7
  output_mode_folder: Final = 0o755
8
8
  output_mode_file: Final = 0o644
@@ -14,7 +14,7 @@ class ReplaceString(NamedTuple):
14
14
  replace: str
15
15
 
16
16
 
17
- def replace_placeholders(file: Path, replaces: List[Tuple[str, str]]):
17
+ def replace_placeholders(file: Path, replaces: list[tuple[str, str]]):
18
18
  with open(file) as f:
19
19
  contents = f.read()
20
20
  for replace in replaces:
@@ -2,7 +2,6 @@ from dynatrace_extension import Extension, Status, StatusValue
2
2
 
3
3
 
4
4
  class ExtensionImpl(Extension):
5
-
6
5
  def query(self):
7
6
  """
8
7
  The query method is automatically scheduled to run every minute
@@ -11,22 +10,24 @@ class ExtensionImpl(Extension):
11
10
 
12
11
  for endpoint in self.activation_config["endpoints"]:
13
12
  url = endpoint["url"]
14
- user = endpoint["user"]
15
- password = endpoint["password"]
13
+ # user = endpoint["user"]
14
+ # password = endpoint["password"]
16
15
  self.logger.debug(f"Running endpoint with url '{url}'")
17
16
 
18
17
  # Your extension code goes here, e.g.
19
18
  # response = requests.get(url, auth=(user, password))
20
19
 
21
20
  # Report metrics with
22
- self.report_metric("my_metric", 1, dimensions={"my_dimension": "dimension1"})
21
+ self.report_metric("metric_key", 1, dimensions={"key": "value"})
23
22
 
24
23
  self.logger.info("query method ended for %extension_name%.")
25
24
 
26
25
  def fastcheck(self) -> Status:
27
26
  """
28
- This is called when the extension runs for the first time.
29
- If this AG cannot run this extension, raise an Exception or return StatusValue.ERROR!
27
+ Use to check if the extension can run.
28
+ If this Activegate cannot run this extension, you can
29
+ raise an Exception or return StatusValue.ERROR.
30
+ This does not run for OneAgent extensions.
30
31
  """
31
32
  return Status(StatusValue.OK)
32
33
 
@@ -35,6 +36,5 @@ def main():
35
36
  ExtensionImpl(name="%extension_name%").run()
36
37
 
37
38
 
38
-
39
- if __name__ == '__main__':
39
+ if __name__ == "__main__":
40
40
  main()
@@ -0,0 +1,77 @@
1
+ exclude = [
2
+ ".bzr",
3
+ ".direnv",
4
+ ".eggs",
5
+ ".git",
6
+ ".git-rewrite",
7
+ ".hg",
8
+ ".ipynb_checkpoints",
9
+ ".mypy_cache",
10
+ ".nox",
11
+ ".pants.d",
12
+ ".pyenv",
13
+ ".pytest_cache",
14
+ ".pytype",
15
+ ".ruff_cache",
16
+ ".svn",
17
+ ".tox",
18
+ ".venv",
19
+ ".vscode",
20
+ "__pypackages__",
21
+ "_build",
22
+ "buck-out",
23
+ "build",
24
+ "dist",
25
+ "node_modules",
26
+ "site-packages",
27
+ "venv",
28
+ "windows.py", # windows run as
29
+ "windows_runas.py", # windows run as
30
+ "*grpc.py", # automatically generated grpc files
31
+ "mureq.py", # a replacement for requests
32
+ "vendor", # vendored dependencies
33
+ "alembic", # alembic migrations
34
+ "oci", # special case for an extension
35
+ "pymqi", # special case for an extension
36
+ "lib" # some extensions commit a lib folder
37
+ ]
38
+
39
+ line-length = 110
40
+ indent-width = 4
41
+
42
+ # Assume Python 3.10
43
+ target-version = "py310"
44
+
45
+ [lint]
46
+ select = ["E", "F", "W", "C90", "I", "N", "UP", "B", "A", "FA", "T20", "Q", "RET", "SIM", "ARG", "PTH", "C"]
47
+ ignore = [
48
+ "T201", # we allow print because these are logged into the extension logs
49
+ "PTH123" # open is used too frequently to open files
50
+ ]
51
+
52
+ # Allow fix for all enabled rules (when `--fix`) is provided.
53
+ fixable = ["ALL"]
54
+ unfixable = []
55
+
56
+ # Allow unused variables when underscore-prefixed.
57
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
58
+
59
+ [lint.mccabe]
60
+ # Flag errors (`C901`) whenever the complexity level exceeds 20.
61
+ max-complexity = 20
62
+
63
+ [format]
64
+ # Like Black, use double quotes for strings.
65
+ quote-style = "double"
66
+
67
+ # Like Black, indent with spaces, rather than tabs.
68
+ indent-style = "space"
69
+
70
+ # Like Black, respect magic trailing commas.
71
+ skip-magic-trailing-comma = false
72
+
73
+ # Like Black, automatically detect the appropriate line ending.
74
+ line-ending = "auto"
75
+
76
+ docstring-code-format = false
77
+ docstring-code-line-length = "dynamic"
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
- from setuptools import setup, find_packages
2
+
3
+ from setuptools import find_packages, setup
3
4
 
4
5
 
5
6
  def find_version() -> str:
@@ -9,20 +10,21 @@ def find_version() -> str:
9
10
  with open(extension_yaml_path, encoding="utf-8") as f:
10
11
  for line in f:
11
12
  if line.startswith("version"):
12
- version = line.split(" ")[-1].strip("\"")
13
+ version = line.split(" ")[-1].strip('"')
13
14
  break
14
15
  except Exception:
15
16
  pass
16
17
  return version
17
18
 
18
19
 
19
- setup(name="%extension_name%",
20
- version=find_version(),
21
- description="%Extension_Name% python EF2 extension",
22
- author="Dynatrace",
23
- packages=find_packages(),
24
- python_requires=">=3.10",
25
- include_package_data=True,
26
- install_requires=["dt-extensions-sdk"],
27
- extras_require={"dev": ["dt-extensions-sdk[cli]"]},
28
- )
20
+ setup(
21
+ name="%extension_name%",
22
+ version=find_version(),
23
+ description="%Extension_Name% python EF2 extension",
24
+ author="Dynatrace",
25
+ packages=find_packages(),
26
+ python_requires=">=3.10",
27
+ include_package_data=True,
28
+ install_requires=["dt-extensions-sdk"],
29
+ extras_require={"dev": ["dt-extensions-sdk[cli]"]},
30
+ )
@@ -4,7 +4,6 @@ import stat
4
4
  import subprocess
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import List, Optional
8
7
 
9
8
  import typer
10
9
  from dtcli.server_api import upload as dt_cli_upload # type: ignore
@@ -91,17 +90,17 @@ def build(
91
90
  "-k",
92
91
  help="Path to the dev fused key-certificate",
93
92
  ),
94
- target_directory: Optional[Path] = typer.Option(None, "--target-directory", "-t"),
95
- extra_platforms: Optional[list[str]] = typer.Option(
93
+ target_directory: Path | None = typer.Option(None, "--target-directory", "-t"),
94
+ extra_platforms: list[str] | None = typer.Option(
96
95
  None, "--extra-platform", "-e", help="Download wheels for an extra platform"
97
96
  ),
98
- extra_index_url: Optional[str] = typer.Option(
97
+ extra_index_url: str | None = typer.Option(
99
98
  None, "--extra-index-url", "-i", help="Extra index url to use when downloading dependencies"
100
99
  ),
101
- find_links: Optional[str] = typer.Option(
100
+ find_links: str | None = typer.Option(
102
101
  None, "--find-links", "-f", help="Extra index url to use when downloading dependencies"
103
102
  ),
104
- only_extra_platforms: Optional[bool] = typer.Option(
103
+ only_extra_platforms: bool | None = typer.Option(
105
104
  False,
106
105
  "--only-extra-platforms",
107
106
  "-o",
@@ -163,7 +162,8 @@ def assemble(
163
162
 
164
163
  # Checks that the module name is valid and exists in the filesystem
165
164
  module_folder = Path(extension_dir) / extension_yaml.python.runtime.module
166
- if not module_folder.exists():
165
+ src_module_folder = Path("src") / module_folder
166
+ if not module_folder.exists() and not src_module_folder.exists():
167
167
  msg = f"Extension module folder {module_folder} not found"
168
168
  raise FileNotFoundError(msg)
169
169
 
@@ -187,16 +187,16 @@ def assemble(
187
187
  @app.command(help="Downloads the dependencies of the extension to the lib folder")
188
188
  def wheel(
189
189
  extension_dir: Path = typer.Argument(".", help="Path to the python extension"),
190
- extra_platforms: Optional[list[str]] = typer.Option(
190
+ extra_platforms: list[str] | None = typer.Option(
191
191
  None, "--extra-platform", "-e", help="Download wheels for an extra platform"
192
192
  ),
193
- extra_index_url: Optional[str] = typer.Option(
193
+ extra_index_url: str | None = typer.Option(
194
194
  None, "--extra-index-url", "-i", help="Extra index url to use when downloading dependencies"
195
195
  ),
196
- find_links: Optional[str] = typer.Option(
196
+ find_links: str | None = typer.Option(
197
197
  None, "--find-links", "-f", help="Extra index url to use when downloading dependencies"
198
198
  ),
199
- only_extra_platforms: Optional[bool] = typer.Option(
199
+ only_extra_platforms: bool | None = typer.Option(
200
200
  False,
201
201
  "--only-extra-platforms",
202
202
  "-o",
@@ -425,9 +425,57 @@ def create(extension_name: str, output: Path = typer.Option(None, "--output", "-
425
425
  console.print(f"Extension created at {extension_path}", style="bold green")
426
426
 
427
427
 
428
- def run_process(
429
- command: List[str], cwd: Optional[Path] = None, env: Optional[dict] = None, print_message: Optional[str] = None
428
+ @app.command(
429
+ "format",
430
+ help="Runs ruff format on the extension code",
431
+ )
432
+ def fmt(extension_dir: Path = typer.Argument(".", help="Path to the python extension")):
433
+ """
434
+ Runs ruff format on the extension code
435
+
436
+ :param extension_dir: The directory of the extension, by default this is the current directory
437
+ """
438
+ run_process(["ruff", "format", str(extension_dir.resolve())])
439
+
440
+
441
+ @app.command(help="Runs ruff check on the extension code")
442
+ def lint(
443
+ extension_dir: Path = typer.Argument(".", help="Path to the python extension"),
444
+ fix: bool = typer.Option(False, "--fix", "-f", help="Fix linting issues"),
430
445
  ):
446
+ """
447
+ Runs ruff lint on the extension code
448
+
449
+ :param extension_dir: The directory of the extension, by default this is the current directory
450
+ :param fix: If true, ask ruff to also fix the linting issues
451
+ """
452
+ command = ["ruff", "check", "--exit-zero"]
453
+ if fix:
454
+ command.append("--fix")
455
+ command.append(str(extension_dir.resolve()))
456
+ run_process(command)
457
+
458
+
459
+ @app.command(help="Adds ruff rules if they don't exist already")
460
+ def ruff_init(extension_dir: Path = typer.Argument(".", help="Path to the python extension")):
461
+ """
462
+ Adds ruff rules if they don't exist already
463
+
464
+ :param extension_dir: The directory of the extension, by default this is the current directory
465
+ """
466
+
467
+ if (extension_dir / "ruff.toml").exists():
468
+ console.print(f"ruff.toml already exists in {extension_dir.resolve()}, skipping", style="bold yellow")
469
+ return
470
+ else:
471
+ # create\extension_template\ruff.toml.template -> extension\ruff.toml
472
+ ruff_template = Path(__file__).parent / "create" / "extension_template" / "ruff.toml.template"
473
+ ruff_file = extension_dir / "ruff.toml"
474
+ shutil.copy(ruff_template, ruff_file)
475
+ console.print(f"Added ruff.toml to {extension_dir.resolve()}", style="bold green")
476
+
477
+
478
+ def run_process(command: list[str], cwd: Path | None = None, env: dict | None = None, print_message: str | None = None):
431
479
  friendly_command = " ".join(command)
432
480
  if print_message is not None:
433
481
  console.print(print_message, style="cyan")
@@ -3,7 +3,6 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
  from enum import Enum
6
- from typing import List
7
6
 
8
7
 
9
8
  class ActivationType(Enum):
@@ -17,7 +16,7 @@ class ActivationConfig(dict):
17
16
  self.version: str = self._activation_context_json.get("version", "")
18
17
  self.enabled: bool = self._activation_context_json.get("enabled", True)
19
18
  self.description: str = self._activation_context_json.get("description", "")
20
- self.feature_sets: List[str] = self._activation_context_json.get("featureSets", [])
19
+ self.feature_sets: list[str] = self._activation_context_json.get("featureSets", [])
21
20
  self.type: ActivationType = ActivationType.REMOTE if self.remote else ActivationType.LOCAL
22
21
  super().__init__()
23
22
 
@@ -4,9 +4,9 @@
4
4
 
5
5
  import logging
6
6
  import random
7
+ from collections.abc import Callable
7
8
  from datetime import datetime, timedelta
8
9
  from timeit import default_timer as timer
9
- from typing import Callable, Dict, Optional, Tuple
10
10
 
11
11
  from .activation import ActivationType
12
12
  from .communication import MultiStatus, Status, StatusValue
@@ -18,10 +18,10 @@ class WrappedCallback:
18
18
  interval: timedelta,
19
19
  callback: Callable,
20
20
  logger: logging.Logger,
21
- args: Optional[Tuple] = None,
22
- kwargs: Optional[Dict] = None,
21
+ args: tuple | None = None,
22
+ kwargs: dict | None = None,
23
23
  running_in_sim=False,
24
- activation_type: Optional[ActivationType] = None,
24
+ activation_type: ActivationType | None = None,
25
25
  ):
26
26
  self.callback: Callable = callback
27
27
  if args is None:
@@ -8,16 +8,17 @@ import json
8
8
  import logging
9
9
  import sys
10
10
  from abc import ABC, abstractmethod
11
+ from collections.abc import Generator, Sequence
11
12
  from dataclasses import dataclass
12
13
  from enum import Enum
13
14
  from pathlib import Path
14
- from typing import Any, Generator, List, Sequence, TypeVar
15
+ from typing import Any, TypeVar
15
16
 
16
17
  from .vendor.mureq.mureq import HTTPException, Response, request
17
18
 
18
19
  CONTENT_TYPE_JSON = "application/json;charset=utf-8"
19
20
  CONTENT_TYPE_PLAIN = "text/plain;charset=utf-8"
20
- COUNT_METRIC_ITEMS_DICT = TypeVar("COUNT_METRIC_ITEMS_DICT", str, List[str])
21
+ COUNT_METRIC_ITEMS_DICT = TypeVar("COUNT_METRIC_ITEMS_DICT", str, list[str])
21
22
 
22
23
  # TODO - I believe these can be adjusted via RuntimeConfig, they can't be constants
23
24
  MAX_MINT_LINES_PER_REQUEST = 1000
@@ -60,7 +61,6 @@ class Status:
60
61
 
61
62
 
62
63
  class MultiStatus:
63
-
64
64
  def __init__(self):
65
65
  self.statuses = []
66
66
 
@@ -352,7 +352,6 @@ class DebugClient(CommunicationClient):
352
352
  local_ingest_port: int = 14499,
353
353
  print_metrics: bool = True,
354
354
  ):
355
-
356
355
  self.secrets = {}
357
356
  if secrets_path and Path(secrets_path).exists():
358
357
  with open(secrets_path) as f:
@@ -9,13 +9,14 @@ import sys
9
9
  import threading
10
10
  import time
11
11
  from argparse import ArgumentParser
12
+ from collections.abc import Callable
12
13
  from concurrent.futures import ThreadPoolExecutor
13
14
  from datetime import datetime, timedelta, timezone
14
15
  from enum import Enum
15
16
  from itertools import chain
16
17
  from pathlib import Path
17
18
  from threading import Lock, RLock, active_count
18
- from typing import Any, Callable, ClassVar, Dict, List, NamedTuple, Optional, Union
19
+ from typing import Any, ClassVar, NamedTuple
19
20
 
20
21
  from .activation import ActivationConfig, ActivationType
21
22
  from .callback import WrappedCallback
@@ -99,7 +100,7 @@ class CountMetricRegistrationEntry(NamedTuple):
99
100
  dimensions_list: list[str]
100
101
 
101
102
  @staticmethod
102
- def make_list(metric_key: str, dimensions_list: List[str]):
103
+ def make_list(metric_key: str, dimensions_list: list[str]):
103
104
  """Build an entry that uses defined list of dimensions for aggregation.
104
105
 
105
106
  Args:
@@ -135,7 +136,7 @@ class CountMetricRegistrationEntry(NamedTuple):
135
136
  return result
136
137
 
137
138
 
138
- def _add_sfm_metric(metric: Metric, sfm_metrics: Optional[List[Metric]] = None):
139
+ def _add_sfm_metric(metric: Metric, sfm_metrics: list[Metric] | None = None):
139
140
  if sfm_metrics is None:
140
141
  sfm_metrics = []
141
142
  metric.validate()
@@ -152,7 +153,7 @@ class Extension:
152
153
  _instance: ClassVar = None
153
154
  schedule_decorators: ClassVar = []
154
155
 
155
- def __new__(cls, *args, **kwargs): # noqa: ARG003
156
+ def __new__(cls, *args, **kwargs): # noqa: ARG004
156
157
  if Extension._instance is None:
157
158
  Extension._instance = super(__class__, cls).__new__(cls)
158
159
  return Extension._instance
@@ -194,24 +195,24 @@ class Extension:
194
195
  self._cluster_time_diff: int = 0
195
196
 
196
197
  # Optional callback to be invoked during the fastcheck
197
- self._fast_check_callback: Optional[Callable[[ActivationConfig, str], Status]] = None
198
+ self._fast_check_callback: Callable[[ActivationConfig, str], Status] | None = None
198
199
 
199
200
  # List of all scheduled callbacks we must run
200
- self._scheduled_callbacks: List[WrappedCallback] = []
201
- self._scheduled_callbacks_before_run: List[WrappedCallback] = []
201
+ self._scheduled_callbacks: list[WrappedCallback] = []
202
+ self._scheduled_callbacks_before_run: list[WrappedCallback] = []
202
203
 
203
204
  # Internal callbacks results, used to report statuses
204
- self._internal_callbacks_results: Dict[str, Status] = {}
205
+ self._internal_callbacks_results: dict[str, Status] = {}
205
206
  self._internal_callbacks_results_lock: Lock = Lock()
206
207
 
207
208
  # Running callbacks, used to get the callback info when reporting metrics
208
- self._running_callbacks: Dict[int, WrappedCallback] = {}
209
+ self._running_callbacks: dict[int, WrappedCallback] = {}
209
210
  self._running_callbacks_lock: Lock = Lock()
210
211
 
211
212
  self._scheduler = sched.scheduler(time.time, time.sleep)
212
213
 
213
214
  # Timestamps for scheduling of internal callbacks
214
- self._next_internal_callbacks_timestamps: Dict[str, datetime] = {
215
+ self._next_internal_callbacks_timestamps: dict[str, datetime] = {
215
216
  "timediff": datetime.now() + TIME_DIFF_INTERVAL,
216
217
  "heartbeat": datetime.now() + HEARTBEAT_INTERVAL,
217
218
  "metrics": datetime.now() + METRIC_SENDING_INTERVAL,
@@ -226,15 +227,15 @@ class Extension:
226
227
 
227
228
  # Extension metrics
228
229
  self._metrics_lock = RLock()
229
- self._metrics: List[str] = []
230
+ self._metrics: list[str] = []
230
231
 
231
232
  # Extension logs
232
233
  self._logs_lock = RLock()
233
- self._logs: List[dict] = []
234
+ self._logs: list[dict] = []
234
235
 
235
236
  # Self monitoring metrics
236
237
  self._sfm_metrics_lock = Lock()
237
- self._callbackSfmReport: Dict[str, WrappedCallback] = {}
238
+ self._callbackSfmReport: dict[str, WrappedCallback] = {}
238
239
 
239
240
  # Count metric delta signals
240
241
  self._delta_signal_buffer: set[str] = set()
@@ -342,9 +343,9 @@ class Extension:
342
343
  def schedule(
343
344
  self,
344
345
  callback: Callable,
345
- interval: Union[timedelta, int],
346
- args: Optional[tuple] = None,
347
- activation_type: Optional[ActivationType] = None,
346
+ interval: timedelta | int,
347
+ args: tuple | None = None,
348
+ activation_type: ActivationType | None = None,
348
349
  ) -> None:
349
350
  """Schedule a method to be executed periodically.
350
351
 
@@ -455,10 +456,11 @@ class Extension:
455
456
  def report_metric(
456
457
  self,
457
458
  key: str,
458
- value: Union[float, str, int, SummaryStat],
459
- dimensions: Optional[Dict[str, str]] = None,
460
- techrule: Optional[str] = None,
461
- timestamp: Optional[datetime] = None,
459
+ value: float | str | int | SummaryStat,
460
+ dimensions: dict[str, str] | None = None,
461
+ device_address: str | None = None,
462
+ techrule: str | None = None,
463
+ timestamp: datetime | None = None,
462
464
  metric_type: MetricType = MetricType.GAUGE,
463
465
  ) -> None:
464
466
  """Report a metric.
@@ -472,6 +474,7 @@ class Extension:
472
474
  key: The metric key, must follow the MINT specification
473
475
  value: The metric value, can be a simple value or a SummaryStat
474
476
  dimensions: A dictionary of dimensions
477
+ device_address: The address of a monitored device/endpoint which produced the metric
475
478
  techrule: The technology rule string set by self.techrule setter.
476
479
  timestamp: The timestamp of the metric, defaults to the current time
477
480
  metric_type: The type of the metric, defaults to MetricType.GAUGE
@@ -483,6 +486,12 @@ class Extension:
483
486
  if "dt.techrule.id" not in dimensions:
484
487
  dimensions["dt.techrule.id"] = techrule
485
488
 
489
+ if device_address:
490
+ if not dimensions:
491
+ dimensions = {}
492
+ if "device.address" not in dimensions:
493
+ dimensions["device.address"] = device_address
494
+
486
495
  if metric_type == MetricType.COUNT and timestamp is None:
487
496
  # We must report a timestamp for count metrics
488
497
  timestamp = datetime.now()
@@ -490,7 +499,7 @@ class Extension:
490
499
  metric = Metric(key=key, value=value, dimensions=dimensions, metric_type=metric_type, timestamp=timestamp)
491
500
  self._add_metric(metric)
492
501
 
493
- def report_mint_lines(self, lines: List[str]) -> None:
502
+ def report_mint_lines(self, lines: list[str]) -> None:
494
503
  """Report mint lines using the MINT protocol
495
504
 
496
505
  Examples:
@@ -507,9 +516,9 @@ class Extension:
507
516
  self,
508
517
  title: str,
509
518
  description: str,
510
- properties: Optional[dict] = None,
511
- timestamp: Optional[datetime] = None,
512
- severity: Union[Severity, str] = Severity.INFO,
519
+ properties: dict | None = None,
520
+ timestamp: datetime | None = None,
521
+ severity: Severity | str = Severity.INFO,
513
522
  send_immediately: bool = False,
514
523
  ) -> None:
515
524
  """Report an event using log ingest.
@@ -543,11 +552,11 @@ class Extension:
543
552
  self,
544
553
  event_type: DtEventType,
545
554
  title: str,
546
- start_time: Optional[int] = None,
547
- end_time: Optional[int] = None,
548
- timeout: Optional[int] = None,
549
- entity_selector: Optional[str] = None,
550
- properties: Optional[dict[str, str]] = None,
555
+ start_time: int | None = None,
556
+ end_time: int | None = None,
557
+ timeout: int | None = None,
558
+ entity_selector: str | None = None,
559
+ properties: dict[str, str] | None = None,
551
560
  ) -> None:
552
561
  """
553
562
  Reports an event using the v2 event ingest API.
@@ -567,7 +576,7 @@ class Extension:
567
576
  entity_selector: The entity selector, if not set, the event is associated with environment entity (optional)
568
577
  properties: A map of event properties (optional)
569
578
  """
570
- event: Dict[str, Any] = {"eventType": event_type, "title": title}
579
+ event: dict[str, Any] = {"eventType": event_type, "title": title}
571
580
  if start_time:
572
581
  event["startTime"] = start_time
573
582
  if end_time:
@@ -654,7 +663,7 @@ class Extension:
654
663
  """
655
664
  self._send_events(log_event, send_immediately=send_immediately)
656
665
 
657
- def report_log_events(self, log_events: List[dict], send_immediately: bool = False):
666
+ def report_log_events(self, log_events: list[dict], send_immediately: bool = False):
658
667
  """Report a list of custom log events using log ingest.
659
668
 
660
669
  Args:
@@ -663,7 +672,7 @@ class Extension:
663
672
  """
664
673
  self._send_events(log_events, send_immediately=send_immediately)
665
674
 
666
- def report_log_lines(self, log_lines: List[Union[str, bytes]], send_immediately: bool = False):
675
+ def report_log_lines(self, log_lines: list[str | bytes], send_immediately: bool = False):
667
676
  """Report a list of log lines using log ingest
668
677
 
669
678
  Args:
@@ -877,13 +886,13 @@ class Extension:
877
886
  api_logger.info(f"Sent {number_of_metrics} metric lines to EEC: {responses}")
878
887
  self._metrics = []
879
888
 
880
- def _prepare_sfm_metrics(self) -> List[str]:
889
+ def _prepare_sfm_metrics(self) -> list[str]:
881
890
  """Prepare self monitoring metrics.
882
891
 
883
892
  Builds the list of mint metric lines to send as self monitoring metrics.
884
893
  """
885
894
 
886
- sfm_metrics: List[Metric] = []
895
+ sfm_metrics: list[Metric] = []
887
896
  sfm_dimensions = {"dt.extension.config.id": self.monitoring_config_id}
888
897
  _add_sfm_metric(
889
898
  SfmMetric("threads", active_count(), sfm_dimensions, client_facing=True, metric_type=MetricType.DELTA),
@@ -1039,11 +1048,11 @@ class Extension:
1039
1048
  with self._metrics_lock:
1040
1049
  self._metrics.append(metric.to_mint_line())
1041
1050
 
1042
- def _add_mint_lines(self, lines: List[str]):
1051
+ def _add_mint_lines(self, lines: list[str]):
1043
1052
  with self._metrics_lock:
1044
1053
  self._metrics.extend(lines)
1045
1054
 
1046
- def _send_events_internal(self, events: Union[dict, List[dict]]):
1055
+ def _send_events_internal(self, events: dict | list[dict]):
1047
1056
  try:
1048
1057
  responses = self._client.send_events(events, self.log_event_enrichment)
1049
1058
 
@@ -1060,7 +1069,7 @@ class Extension:
1060
1069
  with self._internal_callbacks_results_lock:
1061
1070
  self._internal_callbacks_results[self._send_events.__name__] = Status(StatusValue.GENERIC_ERROR, str(e))
1062
1071
 
1063
- def _send_events(self, events: Union[dict, List[dict]], send_immediately: bool = False):
1072
+ def _send_events(self, events: dict | list[dict], send_immediately: bool = False):
1064
1073
  if send_immediately:
1065
1074
  self._internal_executor.submit(self._send_events_internal, events)
1066
1075
  return
@@ -2,8 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
+ from collections.abc import Callable
5
6
  from datetime import datetime, timedelta
6
- from typing import Callable, Dict, List, Optional, Union
7
7
 
8
8
  from .activation import ActivationConfig, ActivationType
9
9
  from .communication import Status
@@ -20,10 +20,10 @@ class _HelperExtension(Extension):
20
20
 
21
21
  def report_metric(
22
22
  key: str,
23
- value: Union[float, str, int, SummaryStat],
24
- dimensions: Optional[Dict[str, str]] = None,
25
- techrule: Optional[str] = None,
26
- timestamp: Optional[datetime] = None,
23
+ value: float | str | int | SummaryStat,
24
+ dimensions: dict[str, str] | None = None,
25
+ techrule: str | None = None,
26
+ timestamp: datetime | None = None,
27
27
  metric_type: MetricType = MetricType.GAUGE,
28
28
  ) -> None:
29
29
  """Reports a metric using the MINT protocol
@@ -37,10 +37,10 @@ def report_metric(
37
37
  :param timestamp: The timestamp of the metric, defaults to the current time
38
38
  :param metric_type: The type of the metric, defaults to MetricType.GAUGE
39
39
  """
40
- _HelperExtension().report_metric(key, value, dimensions, techrule, timestamp, metric_type)
40
+ _HelperExtension().report_metric(key, value, dimensions, techrule, timestamp, metric_type) # type: ignore
41
41
 
42
42
 
43
- def report_mint_lines(lines: List[str]) -> None:
43
+ def report_mint_lines(lines: list[str]) -> None:
44
44
  """Reports mint lines using the MINT protocol.
45
45
  These lines are not validated before being sent.
46
46
 
@@ -52,11 +52,11 @@ def report_mint_lines(lines: List[str]) -> None:
52
52
  def report_dt_event(
53
53
  event_type: DtEventType,
54
54
  title: str,
55
- start_time: Optional[int] = None,
56
- end_time: Optional[int] = None,
57
- timeout: Optional[int] = None,
58
- entity_selector: Optional[str] = None,
59
- properties: Optional[dict[str, str]] = None,
55
+ start_time: int | None = None,
56
+ end_time: int | None = None,
57
+ timeout: int | None = None,
58
+ entity_selector: str | None = None,
59
+ properties: dict[str, str] | None = None,
60
60
  ) -> None:
61
61
  """
62
62
  Reports a custom event v2 using event ingest
@@ -85,9 +85,9 @@ def report_dt_event_dict(event: dict):
85
85
 
86
86
  def schedule(
87
87
  callback: Callable,
88
- interval: Union[timedelta, int],
89
- args: Optional[tuple] = None,
90
- activation_type: Optional[ActivationType] = None,
88
+ interval: timedelta | int,
89
+ args: tuple | None = None,
90
+ activation_type: ActivationType | None = None,
91
91
  ) -> None:
92
92
  """Schedules a callback to be called periodically
93
93
 
@@ -101,7 +101,7 @@ def schedule(
101
101
 
102
102
 
103
103
  def schedule_function(
104
- interval: Union[timedelta, int], args: Optional[tuple] = None, activation_type: Optional[ActivationType] = None
104
+ interval: timedelta | int, args: tuple | None = None, activation_type: ActivationType | None = None
105
105
  ):
106
106
  def decorator(function):
107
107
  schedule(function, interval, args=args, activation_type=activation_type)
@@ -110,7 +110,7 @@ def schedule_function(
110
110
 
111
111
 
112
112
  def schedule_method(
113
- interval: Union[timedelta, int], args: Optional[tuple] = None, activation_type: Optional[ActivationType] = None
113
+ interval: timedelta | int, args: tuple | None = None, activation_type: ActivationType | None = None
114
114
  ):
115
115
  def decorator(function):
116
116
  Extension.schedule_decorators.append((function, interval, args, activation_type))
@@ -121,9 +121,9 @@ def schedule_method(
121
121
  def report_event(
122
122
  title: str,
123
123
  description: str,
124
- properties: Optional[dict] = None,
125
- timestamp: Optional[datetime] = None,
126
- severity: Union[Severity, str] = Severity.INFO,
124
+ properties: dict | None = None,
125
+ timestamp: datetime | None = None,
126
+ severity: Severity | str = Severity.INFO,
127
127
  ) -> None:
128
128
  """Reports an event using the MINT protocol
129
129
 
@@ -144,7 +144,7 @@ def report_log_event(log_event: dict):
144
144
  _HelperExtension().report_log_event(log_event)
145
145
 
146
146
 
147
- def report_log_events(log_events: List[dict]):
147
+ def report_log_events(log_events: list[dict]):
148
148
  """Reports a list of custom log events using log ingest
149
149
 
150
150
  :param log_events: The list of log events
@@ -152,7 +152,7 @@ def report_log_events(log_events: List[dict]):
152
152
  _HelperExtension().report_log_events(log_events)
153
153
 
154
154
 
155
- def report_log_lines(log_lines: List[Union[str, bytes]]):
155
+ def report_log_lines(log_lines: list[str | bytes]):
156
156
  """Reports a list of log lines using log ingest
157
157
 
158
158
  :param log_lines: The list of log lines
@@ -4,7 +4,6 @@
4
4
 
5
5
  from datetime import datetime
6
6
  from enum import Enum
7
- from typing import Dict, Optional, Union
8
7
 
9
8
  # https://bitbucket.lab.dynatrace.org/projects/ONE/repos/schemaless-metrics-spec/browse/limits.md
10
9
  LIMIT_DIMENSIONS_COUNT = 50
@@ -41,18 +40,18 @@ class Metric:
41
40
  def __init__(
42
41
  self,
43
42
  key: str,
44
- value: Union[float, int, str, SummaryStat],
45
- dimensions: Optional[Dict[str, str]] = None,
43
+ value: float | int | str | SummaryStat,
44
+ dimensions: dict[str, str] | None = None,
46
45
  metric_type: MetricType = MetricType.GAUGE,
47
- timestamp: Optional[datetime] = None,
46
+ timestamp: datetime | None = None,
48
47
  ):
49
48
  self.key: str = key
50
- self.value: Union[float, int, str, SummaryStat] = value
49
+ self.value: float | int | str | SummaryStat = value
51
50
  if dimensions is None:
52
51
  dimensions = {}
53
- self.dimensions: Dict[str, str] = dimensions
52
+ self.dimensions: dict[str, str] = dimensions
54
53
  self.metric_type: MetricType = metric_type
55
- self.timestamp: Optional[datetime] = timestamp
54
+ self.timestamp: datetime | None = timestamp
56
55
 
57
56
  def __hash__(self):
58
57
  return hash(self._key_and_dimensions())
@@ -103,10 +102,10 @@ class SfmMetric(Metric):
103
102
  def __init__(
104
103
  self,
105
104
  key: str,
106
- value: Union[float, int, str, SummaryStat],
107
- dimensions: Optional[Dict[str, str]] = None,
105
+ value: float | int | str | SummaryStat,
106
+ dimensions: dict[str, str] | None = None,
108
107
  metric_type: MetricType = MetricType.GAUGE,
109
- timestamp: Optional[datetime] = None,
108
+ timestamp: datetime | None = None,
110
109
  client_facing: bool = False,
111
110
  ):
112
111
  key = create_sfm_metric_key(key, client_facing)
@@ -3,7 +3,7 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
5
  import logging
6
- from typing import ClassVar, List, NamedTuple
6
+ from typing import ClassVar, NamedTuple
7
7
 
8
8
 
9
9
  class DefaultLogLevel(NamedTuple):
@@ -25,7 +25,7 @@ class RuntimeProperties:
25
25
  self.userconfig: str = json_response.get("userconfig", "")
26
26
  self.debugmode: bool = json_response.get("debugmode", "0") == "1"
27
27
  self.runtime: dict = json_response.get("runtime", {})
28
- self.tasks: List[str] = json_response.get("tasks", [])
28
+ self.tasks: list[str] = json_response.get("tasks", [])
29
29
 
30
30
  @classmethod
31
31
  def set_default_log_level(cls, value: str):
@@ -14,12 +14,14 @@ PREFIX_PGI = "PROCESS_GROUP_INSTANCE"
14
14
  class EntryProperties:
15
15
  technologies: list[str]
16
16
  pg_technologies: list[str]
17
+ extra_properties: dict[str, str]
17
18
 
18
19
  @staticmethod
19
- def from_json(json_data: dict) -> EntryProperties:
20
+ def from_json(json_data: dict[str, str]) -> EntryProperties:
20
21
  technologies = json_data.get("Technologies", "").split(",")
21
22
  pg_technologies = json_data.get("pgTechnologies", "").split(",")
22
- return EntryProperties(technologies, pg_technologies)
23
+ extra_properties = {k: v for k, v in json_data.items() if k not in ["Technologies", "pgTechnologies"]}
24
+ return EntryProperties(technologies, pg_technologies, extra_properties)
23
25
 
24
26
 
25
27
  @dataclass
@@ -111,15 +113,23 @@ class Entry:
111
113
  processes = [Process.from_json(p) for p in json_data.get("processes", [])]
112
114
 
113
115
  # The structure here was never thought out, so we have to check for both keys and merge them into one object
114
- properties_list = json_data.get("properties", [])
115
- technologies = [p for p in properties_list if "Technologies" in p]
116
- if technologies:
117
- technologies = technologies[0]["Technologies"].split(",")
118
-
119
- pg_technologies = [p for p in properties_list if "pgTechnologies" in p]
120
- if pg_technologies:
121
- pg_technologies = pg_technologies[0]["pgTechnologies"].split(",")
122
- properties = EntryProperties(technologies or [], pg_technologies or [])
116
+ properties_list: list[dict[str, str]] = json_data.get("properties", [])
117
+ technologies = []
118
+ pg_technologies = []
119
+ # There may be other useful properties included such as: mssql_instance_name.
120
+ extra_properties = {}
121
+
122
+ for prop in properties_list:
123
+ for key, value in prop.items():
124
+ match key:
125
+ case "Technologies":
126
+ technologies.extend(value.split(","))
127
+ case "pgTechnologies":
128
+ pg_technologies.extend(value.split(","))
129
+ case _:
130
+ extra_properties[key] = value
131
+
132
+ properties = EntryProperties(technologies, pg_technologies, extra_properties)
123
133
 
124
134
  return Entry(group_id, node_id, group_instance_id, process_type, group_name, processes, properties)
125
135