mlrun 1.7.0rc22__py3-none-any.whl → 1.7.0rc28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mlrun might be problematic. Click here for more details.
- mlrun/__main__.py +10 -8
- mlrun/alerts/alert.py +13 -1
- mlrun/artifacts/manager.py +5 -0
- mlrun/common/constants.py +2 -2
- mlrun/common/formatters/__init__.py +1 -0
- mlrun/common/formatters/artifact.py +26 -3
- mlrun/common/formatters/base.py +9 -9
- mlrun/common/formatters/run.py +26 -0
- mlrun/common/helpers.py +11 -0
- mlrun/common/schemas/__init__.py +4 -0
- mlrun/common/schemas/alert.py +5 -9
- mlrun/common/schemas/api_gateway.py +64 -16
- mlrun/common/schemas/artifact.py +11 -0
- mlrun/common/schemas/constants.py +3 -0
- mlrun/common/schemas/feature_store.py +58 -28
- mlrun/common/schemas/model_monitoring/constants.py +21 -12
- mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -12
- mlrun/common/schemas/pipeline.py +16 -0
- mlrun/common/schemas/project.py +17 -0
- mlrun/common/schemas/runs.py +17 -0
- mlrun/common/schemas/schedule.py +1 -1
- mlrun/common/types.py +5 -0
- mlrun/config.py +10 -25
- mlrun/datastore/azure_blob.py +2 -1
- mlrun/datastore/datastore.py +3 -3
- mlrun/datastore/google_cloud_storage.py +6 -2
- mlrun/datastore/snowflake_utils.py +3 -1
- mlrun/datastore/sources.py +26 -11
- mlrun/datastore/store_resources.py +2 -0
- mlrun/datastore/targets.py +68 -16
- mlrun/db/base.py +64 -2
- mlrun/db/httpdb.py +129 -41
- mlrun/db/nopdb.py +44 -3
- mlrun/errors.py +5 -3
- mlrun/execution.py +18 -10
- mlrun/feature_store/retrieval/spark_merger.py +2 -1
- mlrun/frameworks/__init__.py +0 -6
- mlrun/model.py +23 -0
- mlrun/model_monitoring/api.py +6 -52
- mlrun/model_monitoring/applications/histogram_data_drift.py +1 -1
- mlrun/model_monitoring/db/stores/__init__.py +37 -24
- mlrun/model_monitoring/db/stores/base/store.py +40 -1
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +42 -87
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +27 -35
- mlrun/model_monitoring/db/tsdb/__init__.py +15 -15
- mlrun/model_monitoring/db/tsdb/base.py +1 -1
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +6 -4
- mlrun/model_monitoring/helpers.py +17 -9
- mlrun/model_monitoring/stream_processing.py +9 -11
- mlrun/model_monitoring/writer.py +11 -11
- mlrun/package/__init__.py +1 -13
- mlrun/package/packagers/__init__.py +1 -6
- mlrun/projects/pipelines.py +10 -9
- mlrun/projects/project.py +95 -81
- mlrun/render.py +10 -5
- mlrun/run.py +13 -8
- mlrun/runtimes/base.py +11 -4
- mlrun/runtimes/daskjob.py +7 -1
- mlrun/runtimes/local.py +16 -3
- mlrun/runtimes/nuclio/application/application.py +0 -2
- mlrun/runtimes/nuclio/function.py +20 -0
- mlrun/runtimes/nuclio/serving.py +9 -6
- mlrun/runtimes/pod.py +5 -29
- mlrun/serving/routers.py +75 -59
- mlrun/serving/server.py +11 -0
- mlrun/serving/states.py +29 -0
- mlrun/serving/v2_serving.py +62 -39
- mlrun/utils/helpers.py +39 -1
- mlrun/utils/logger.py +36 -2
- mlrun/utils/notifications/notification/base.py +43 -7
- mlrun/utils/notifications/notification/git.py +21 -0
- mlrun/utils/notifications/notification/slack.py +9 -14
- mlrun/utils/notifications/notification/webhook.py +41 -1
- mlrun/utils/notifications/notification_pusher.py +3 -9
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/METADATA +12 -7
- {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/RECORD +81 -80
- {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/WHEEL +1 -1
- {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc22.dist-info → mlrun-1.7.0rc28.dist-info}/top_level.txt +0 -0
|
@@ -28,6 +28,10 @@ class NotificationBase:
|
|
|
28
28
|
self.name = name
|
|
29
29
|
self.params = params or {}
|
|
30
30
|
|
|
31
|
+
@classmethod
|
|
32
|
+
def validate_params(cls, params):
|
|
33
|
+
pass
|
|
34
|
+
|
|
31
35
|
@property
|
|
32
36
|
def active(self) -> bool:
|
|
33
37
|
return True
|
|
@@ -69,16 +73,27 @@ class NotificationBase:
|
|
|
69
73
|
if custom_html:
|
|
70
74
|
return custom_html
|
|
71
75
|
|
|
72
|
-
if self.name:
|
|
73
|
-
message = f"{self.name}: {message}"
|
|
74
|
-
|
|
75
76
|
if alert:
|
|
76
77
|
if not event_data:
|
|
77
78
|
return f"[{severity}] {message}"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
|
|
80
|
+
html = f"<h3>[{severity}] {message}</h3>"
|
|
81
|
+
html += f"<br>{alert.name} alert has occurred<br>"
|
|
82
|
+
html += f"<br><h4>Project:</h4>{alert.project}<br>"
|
|
83
|
+
html += f"<br><h4>ID:</h4>{event_data.entity.ids[0]}<br>"
|
|
84
|
+
html += f"<br><h4>Summary:</h4>{mlrun.utils.helpers.format_alert_summary(alert, event_data)}<br>"
|
|
85
|
+
|
|
86
|
+
if event_data.value_dict:
|
|
87
|
+
html += "<br><h4>Event data:</h4>"
|
|
88
|
+
for key, value in event_data.value_dict.items():
|
|
89
|
+
html += f"{key}: {value}<br>"
|
|
90
|
+
|
|
91
|
+
overview_type, url = self._get_overview_type_and_url(alert, event_data)
|
|
92
|
+
html += f"<br><h4>Overview:</h4><a href={url}>{overview_type}</a>"
|
|
93
|
+
return html
|
|
94
|
+
|
|
95
|
+
if self.name:
|
|
96
|
+
message = f"{self.name}: {message}"
|
|
82
97
|
|
|
83
98
|
if not runs:
|
|
84
99
|
return f"[{severity}] {message}"
|
|
@@ -90,3 +105,24 @@ class NotificationBase:
|
|
|
90
105
|
html += "<br>click the hyper links below to see detailed results<br>"
|
|
91
106
|
html += runs.show(display=False, short=True)
|
|
92
107
|
return html
|
|
108
|
+
|
|
109
|
+
def _get_overview_type_and_url(
|
|
110
|
+
self,
|
|
111
|
+
alert: mlrun.common.schemas.AlertConfig,
|
|
112
|
+
event_data: mlrun.common.schemas.Event,
|
|
113
|
+
) -> (str, str):
|
|
114
|
+
if (
|
|
115
|
+
event_data.entity.kind == mlrun.common.schemas.alert.EventEntityKind.JOB
|
|
116
|
+
): # JOB entity
|
|
117
|
+
uid = event_data.value_dict.get("uid")
|
|
118
|
+
url = mlrun.utils.helpers.get_ui_url(alert.project, uid)
|
|
119
|
+
overview_type = "Job overview"
|
|
120
|
+
else: # MODEL entity
|
|
121
|
+
model_name = event_data.value_dict.get("model")
|
|
122
|
+
model_endpoint_id = event_data.value_dict.get("model_endpoint_id")
|
|
123
|
+
url = mlrun.utils.helpers.get_model_endpoint_url(
|
|
124
|
+
alert.project, model_name, model_endpoint_id
|
|
125
|
+
)
|
|
126
|
+
overview_type = "Model endpoint"
|
|
127
|
+
|
|
128
|
+
return overview_type, url
|
|
@@ -30,6 +30,27 @@ class GitNotification(NotificationBase):
|
|
|
30
30
|
API/Client notification for setting a rich run statuses git issue comment (github/gitlab)
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
+
@classmethod
|
|
34
|
+
def validate_params(cls, params):
|
|
35
|
+
git_repo = params.get("repo", None)
|
|
36
|
+
git_issue = params.get("issue", None)
|
|
37
|
+
git_merge_request = params.get("merge_request", None)
|
|
38
|
+
token = (
|
|
39
|
+
params.get("token", None)
|
|
40
|
+
or params.get("GIT_TOKEN", None)
|
|
41
|
+
or params.get("GITHUB_TOKEN", None)
|
|
42
|
+
)
|
|
43
|
+
if not git_repo:
|
|
44
|
+
raise ValueError("Parameter 'repo' is required for GitNotification")
|
|
45
|
+
|
|
46
|
+
if not token:
|
|
47
|
+
raise ValueError("Parameter 'token' is required for GitNotification")
|
|
48
|
+
|
|
49
|
+
if not git_issue and not git_merge_request:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"At least one of 'issue' or 'merge_request' is required for GitNotification"
|
|
52
|
+
)
|
|
53
|
+
|
|
33
54
|
async def push(
|
|
34
55
|
self,
|
|
35
56
|
message: str,
|
|
@@ -35,6 +35,14 @@ class SlackNotification(NotificationBase):
|
|
|
35
35
|
"skipped": ":zzz:",
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate_params(cls, params):
|
|
40
|
+
webhook = params.get("webhook", None) or mlrun.get_secret_or_env(
|
|
41
|
+
"SLACK_WEBHOOK"
|
|
42
|
+
)
|
|
43
|
+
if not webhook:
|
|
44
|
+
raise ValueError("Parameter 'webhook' is required for SlackNotification")
|
|
45
|
+
|
|
38
46
|
async def push(
|
|
39
47
|
self,
|
|
40
48
|
message: str,
|
|
@@ -153,20 +161,7 @@ class SlackNotification(NotificationBase):
|
|
|
153
161
|
data_text = "\n".join(data_lines)
|
|
154
162
|
line.append(self._get_slack_row(f"*Event data:*\n{data_text}"))
|
|
155
163
|
|
|
156
|
-
|
|
157
|
-
event_data.entity.kind == mlrun.common.schemas.alert.EventEntityKind.JOB
|
|
158
|
-
): # JOB entity
|
|
159
|
-
uid = event_data.value_dict.get("uid")
|
|
160
|
-
url = mlrun.utils.helpers.get_ui_url(alert.project, uid)
|
|
161
|
-
overview_type = "Job overview"
|
|
162
|
-
else: # MODEL entity
|
|
163
|
-
model_name = event_data.value_dict.get("model")
|
|
164
|
-
model_endpoint_id = event_data.value_dict.get("model_endpoint_id")
|
|
165
|
-
url = mlrun.utils.helpers.get_model_endpoint_url(
|
|
166
|
-
alert.project, model_name, model_endpoint_id
|
|
167
|
-
)
|
|
168
|
-
overview_type = "Model endpoint"
|
|
169
|
-
|
|
164
|
+
overview_type, url = self._get_overview_type_and_url(alert, event_data)
|
|
170
165
|
line.append(self._get_slack_row(f"*Overview:*\n<{url}|*{overview_type}*>"))
|
|
171
166
|
|
|
172
167
|
return line
|
|
@@ -28,6 +28,12 @@ class WebhookNotification(NotificationBase):
|
|
|
28
28
|
API/Client notification for sending run statuses in a http request
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
+
@classmethod
|
|
32
|
+
def validate_params(cls, params):
|
|
33
|
+
url = params.get("url", None)
|
|
34
|
+
if not url:
|
|
35
|
+
raise ValueError("Parameter 'url' is required for WebhookNotification")
|
|
36
|
+
|
|
31
37
|
async def push(
|
|
32
38
|
self,
|
|
33
39
|
message: str,
|
|
@@ -63,7 +69,7 @@ class WebhookNotification(NotificationBase):
|
|
|
63
69
|
request_body["custom_html"] = custom_html
|
|
64
70
|
|
|
65
71
|
if override_body:
|
|
66
|
-
request_body = override_body
|
|
72
|
+
request_body = self._serialize_runs_in_request_body(override_body, runs)
|
|
67
73
|
|
|
68
74
|
# Specify the `verify_ssl` parameter value only for HTTPS urls.
|
|
69
75
|
# The `ClientSession` allows using `ssl=None` for the default SSL check,
|
|
@@ -77,3 +83,37 @@ class WebhookNotification(NotificationBase):
|
|
|
77
83
|
url, headers=headers, json=request_body, ssl=verify_ssl
|
|
78
84
|
)
|
|
79
85
|
response.raise_for_status()
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _serialize_runs_in_request_body(override_body, runs):
|
|
89
|
+
str_parsed_runs = ""
|
|
90
|
+
runs = runs or []
|
|
91
|
+
|
|
92
|
+
def parse_runs():
|
|
93
|
+
parsed_runs = []
|
|
94
|
+
for run in runs:
|
|
95
|
+
if hasattr(run, "to_dict"):
|
|
96
|
+
run = run.to_dict()
|
|
97
|
+
if isinstance(run, dict):
|
|
98
|
+
parsed_run = {
|
|
99
|
+
"project": run["metadata"]["project"],
|
|
100
|
+
"name": run["metadata"]["name"],
|
|
101
|
+
"host": run["metadata"]["labels"]["host"],
|
|
102
|
+
"status": {"state": run["status"]["state"]},
|
|
103
|
+
}
|
|
104
|
+
if run["status"].get("error", None):
|
|
105
|
+
parsed_run["status"]["error"] = run["status"]["error"]
|
|
106
|
+
elif run["status"].get("results", None):
|
|
107
|
+
parsed_run["status"]["results"] = run["status"]["results"]
|
|
108
|
+
parsed_runs.append(parsed_run)
|
|
109
|
+
return str(parsed_runs)
|
|
110
|
+
|
|
111
|
+
if isinstance(override_body, dict):
|
|
112
|
+
for key, value in override_body.items():
|
|
113
|
+
if "{{ runs }}" or "{{runs}}" in value:
|
|
114
|
+
if not str_parsed_runs:
|
|
115
|
+
str_parsed_runs = parse_runs()
|
|
116
|
+
override_body[key] = value.replace(
|
|
117
|
+
"{{ runs }}", str_parsed_runs
|
|
118
|
+
).replace("{{runs}}", str_parsed_runs)
|
|
119
|
+
return override_body
|
|
@@ -20,9 +20,9 @@ import traceback
|
|
|
20
20
|
import typing
|
|
21
21
|
from concurrent.futures import ThreadPoolExecutor
|
|
22
22
|
|
|
23
|
-
import kfp
|
|
24
23
|
import mlrun_pipelines.common.ops
|
|
25
24
|
import mlrun_pipelines.models
|
|
25
|
+
import mlrun_pipelines.utils
|
|
26
26
|
|
|
27
27
|
import mlrun.common.constants as mlrun_constants
|
|
28
28
|
import mlrun.common.runtimes.constants
|
|
@@ -397,7 +397,7 @@ class NotificationPusher(_NotificationPusherBase):
|
|
|
397
397
|
try:
|
|
398
398
|
_run = db.list_runs(
|
|
399
399
|
project=run.metadata.project,
|
|
400
|
-
labels=f"mlrun_constants.MLRunInternalLabels.runner_pod={_step.node_name}",
|
|
400
|
+
labels=f"{mlrun_constants.MLRunInternalLabels.runner_pod}={_step.node_name}",
|
|
401
401
|
)[0]
|
|
402
402
|
except IndexError:
|
|
403
403
|
_run = {
|
|
@@ -484,13 +484,7 @@ class NotificationPusher(_NotificationPusherBase):
|
|
|
484
484
|
def _get_workflow_manifest(
|
|
485
485
|
workflow_id: str,
|
|
486
486
|
) -> typing.Optional[mlrun_pipelines.models.PipelineManifest]:
|
|
487
|
-
|
|
488
|
-
if not kfp_url:
|
|
489
|
-
raise mlrun.errors.MLRunNotFoundError(
|
|
490
|
-
"KubeFlow Pipelines is not configured"
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
kfp_client = kfp.Client(host=kfp_url)
|
|
487
|
+
kfp_client = mlrun_pipelines.utils.get_client(mlrun.mlconf)
|
|
494
488
|
|
|
495
489
|
# arbitrary timeout of 5 seconds, the workflow should be done by now
|
|
496
490
|
kfp_run = kfp_client.wait_for_run_completion(workflow_id, 5)
|
mlrun/utils/version/version.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mlrun
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.0rc28
|
|
4
4
|
Summary: Tracking and config of machine learning runs
|
|
5
5
|
Home-page: https://github.com/mlrun/mlrun
|
|
6
6
|
Author: Yaron Haviv
|
|
@@ -28,30 +28,30 @@ Requires-Dist: aiohttp-retry ~=2.8
|
|
|
28
28
|
Requires-Dist: click ~=8.1
|
|
29
29
|
Requires-Dist: nest-asyncio ~=1.0
|
|
30
30
|
Requires-Dist: ipython ~=8.10
|
|
31
|
-
Requires-Dist: nuclio-jupyter ~=0.
|
|
31
|
+
Requires-Dist: nuclio-jupyter ~=0.10.0
|
|
32
32
|
Requires-Dist: numpy <1.27.0,>=1.16.5
|
|
33
33
|
Requires-Dist: pandas <2.2,>=1.2
|
|
34
34
|
Requires-Dist: pyarrow <15,>=10.0
|
|
35
|
-
Requires-Dist: pyyaml
|
|
35
|
+
Requires-Dist: pyyaml <7,>=5.4.1
|
|
36
36
|
Requires-Dist: requests ~=2.31
|
|
37
37
|
Requires-Dist: tabulate ~=0.8.6
|
|
38
38
|
Requires-Dist: v3io ~=0.6.4
|
|
39
39
|
Requires-Dist: pydantic <1.10.15,>=1.10.8
|
|
40
40
|
Requires-Dist: mergedeep ~=1.3
|
|
41
|
-
Requires-Dist: v3io-frames ~=0.10.
|
|
41
|
+
Requires-Dist: v3io-frames ~=0.10.14
|
|
42
42
|
Requires-Dist: semver ~=3.0
|
|
43
43
|
Requires-Dist: dependency-injector ~=4.41
|
|
44
44
|
Requires-Dist: fsspec <2024.4,>=2023.9.2
|
|
45
45
|
Requires-Dist: v3iofs ~=0.1.17
|
|
46
|
-
Requires-Dist: storey ~=1.7.
|
|
46
|
+
Requires-Dist: storey ~=1.7.20
|
|
47
47
|
Requires-Dist: inflection ~=0.5.0
|
|
48
48
|
Requires-Dist: python-dotenv ~=0.17.0
|
|
49
49
|
Requires-Dist: setuptools ~=69.1
|
|
50
50
|
Requires-Dist: deprecated ~=1.2
|
|
51
51
|
Requires-Dist: jinja2 >=3.1.3,~=3.1
|
|
52
52
|
Requires-Dist: orjson <4,>=3.9.15
|
|
53
|
-
Requires-Dist: mlrun-pipelines-kfp-common
|
|
54
|
-
Requires-Dist: mlrun-pipelines-kfp-v1-8
|
|
53
|
+
Requires-Dist: mlrun-pipelines-kfp-common ~=0.1.2
|
|
54
|
+
Requires-Dist: mlrun-pipelines-kfp-v1-8 ~=0.1.2
|
|
55
55
|
Provides-Extra: alibaba-oss
|
|
56
56
|
Requires-Dist: ossfs ==2023.12.0 ; extra == 'alibaba-oss'
|
|
57
57
|
Requires-Dist: oss2 ==2.18.1 ; extra == 'alibaba-oss'
|
|
@@ -81,6 +81,7 @@ Requires-Dist: plotly <5.12.0,~=5.4 ; extra == 'all'
|
|
|
81
81
|
Requires-Dist: pyopenssl >=23 ; extra == 'all'
|
|
82
82
|
Requires-Dist: redis ~=4.3 ; extra == 'all'
|
|
83
83
|
Requires-Dist: s3fs <2024.4,>=2023.9.2 ; extra == 'all'
|
|
84
|
+
Requires-Dist: snowflake-connector-python ~=3.7 ; extra == 'all'
|
|
84
85
|
Requires-Dist: sqlalchemy ~=1.4 ; extra == 'all'
|
|
85
86
|
Requires-Dist: taos-ws-py ~=0.3.2 ; extra == 'all'
|
|
86
87
|
Provides-Extra: api
|
|
@@ -130,6 +131,7 @@ Requires-Dist: plotly <5.12.0,~=5.4 ; extra == 'complete'
|
|
|
130
131
|
Requires-Dist: pyopenssl >=23 ; extra == 'complete'
|
|
131
132
|
Requires-Dist: redis ~=4.3 ; extra == 'complete'
|
|
132
133
|
Requires-Dist: s3fs <2024.4,>=2023.9.2 ; extra == 'complete'
|
|
134
|
+
Requires-Dist: snowflake-connector-python ~=3.7 ; extra == 'complete'
|
|
133
135
|
Requires-Dist: sqlalchemy ~=1.4 ; extra == 'complete'
|
|
134
136
|
Requires-Dist: taos-ws-py ~=0.3.2 ; extra == 'complete'
|
|
135
137
|
Provides-Extra: complete-api
|
|
@@ -164,6 +166,7 @@ Requires-Dist: pymysql ~=1.0 ; extra == 'complete-api'
|
|
|
164
166
|
Requires-Dist: pyopenssl >=23 ; extra == 'complete-api'
|
|
165
167
|
Requires-Dist: redis ~=4.3 ; extra == 'complete-api'
|
|
166
168
|
Requires-Dist: s3fs <2024.4,>=2023.9.2 ; extra == 'complete-api'
|
|
169
|
+
Requires-Dist: snowflake-connector-python ~=3.7 ; extra == 'complete-api'
|
|
167
170
|
Requires-Dist: sqlalchemy ~=1.4 ; extra == 'complete-api'
|
|
168
171
|
Requires-Dist: taos-ws-py ~=0.3.2 ; extra == 'complete-api'
|
|
169
172
|
Requires-Dist: timelength ~=1.1 ; extra == 'complete-api'
|
|
@@ -196,6 +199,8 @@ Provides-Extra: s3
|
|
|
196
199
|
Requires-Dist: boto3 <1.29.0,>=1.28.0 ; extra == 's3'
|
|
197
200
|
Requires-Dist: aiobotocore <2.8,>=2.5.0 ; extra == 's3'
|
|
198
201
|
Requires-Dist: s3fs <2024.4,>=2023.9.2 ; extra == 's3'
|
|
202
|
+
Provides-Extra: snowflake
|
|
203
|
+
Requires-Dist: snowflake-connector-python ~=3.7 ; extra == 'snowflake'
|
|
199
204
|
Provides-Extra: sqlalchemy
|
|
200
205
|
Requires-Dist: sqlalchemy ~=1.4 ; extra == 'sqlalchemy'
|
|
201
206
|
Provides-Extra: tdengine
|