polaris-python 0.0.1.dev3__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.
Files changed (84) hide show
  1. polaris/__init__.py +74 -0
  2. polaris/_version.py +16 -0
  3. polaris/api/__init__.py +55 -0
  4. polaris/api/_log_file.py +232 -0
  5. polaris/api/config.py +145 -0
  6. polaris/api/consumer.py +240 -0
  7. polaris/api/log.py +414 -0
  8. polaris/api/lossless.py +73 -0
  9. polaris/api/provider.py +152 -0
  10. polaris/api/skill.py +162 -0
  11. polaris/pkg/__init__.py +15 -0
  12. polaris/pkg/common/__init__.py +15 -0
  13. polaris/pkg/common/plugin.py +33 -0
  14. polaris/pkg/config/__init__.py +15 -0
  15. polaris/pkg/config/api.py +1205 -0
  16. polaris/pkg/config/default.py +92 -0
  17. polaris/pkg/config/event.py +76 -0
  18. polaris/pkg/config/loader.py +392 -0
  19. polaris/pkg/config/stat_reporter.py +100 -0
  20. polaris/pkg/flow/__init__.py +15 -0
  21. polaris/pkg/flow/admin_server.py +104 -0
  22. polaris/pkg/flow/async_flow.py +55 -0
  23. polaris/pkg/flow/base_flow.py +111 -0
  24. polaris/pkg/flow/impl.py +1277 -0
  25. polaris/pkg/flow/lossless.py +605 -0
  26. polaris/pkg/flow/notify.py +46 -0
  27. polaris/pkg/flow/register.py +210 -0
  28. polaris/pkg/flow/skill.py +110 -0
  29. polaris/pkg/flow/sync_flow.py +20 -0
  30. polaris/pkg/model/__init__.py +15 -0
  31. polaris/pkg/model/context.py +53 -0
  32. polaris/pkg/model/contract.py +89 -0
  33. polaris/pkg/model/core.py +51 -0
  34. polaris/pkg/model/engine.py +83 -0
  35. polaris/pkg/model/error.py +94 -0
  36. polaris/pkg/model/event.py +159 -0
  37. polaris/pkg/model/lossless.py +102 -0
  38. polaris/pkg/model/rule.py +152 -0
  39. polaris/pkg/model/server.py +288 -0
  40. polaris/pkg/model/service.py +820 -0
  41. polaris/pkg/model/skill.py +179 -0
  42. polaris/pkg/model/stat.py +168 -0
  43. polaris/pkg/model/util.py +58 -0
  44. polaris/pkg/version/__init__.py +18 -0
  45. polaris/plugin/__init__.py +32 -0
  46. polaris/plugin/circuit_breaker/__init__.py +19 -0
  47. polaris/plugin/circuit_breaker/base.py +54 -0
  48. polaris/plugin/circuit_breaker/composite.py +265 -0
  49. polaris/plugin/circuit_breaker/config.py +36 -0
  50. polaris/plugin/event/__init__.py +21 -0
  51. polaris/plugin/event/pushgateway.py +202 -0
  52. polaris/plugin/load_balancer/__init__.py +24 -0
  53. polaris/plugin/load_balancer/base.py +30 -0
  54. polaris/plugin/load_balancer/random.py +39 -0
  55. polaris/plugin/load_balancer/weighted_random.py +50 -0
  56. polaris/plugin/local_registry/__init__.py +18 -0
  57. polaris/plugin/local_registry/base.py +47 -0
  58. polaris/plugin/local_registry/file_cache.py +335 -0
  59. polaris/plugin/local_registry/local_cache.py +433 -0
  60. polaris/plugin/server_connector/__init__.py +20 -0
  61. polaris/plugin/server_connector/base.py +104 -0
  62. polaris/plugin/server_connector/convert.py +149 -0
  63. polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
  64. polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
  65. polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
  66. polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
  67. polaris/plugin/service_router/__init__.py +28 -0
  68. polaris/plugin/service_router/base.py +105 -0
  69. polaris/plugin/service_router/isolated.py +35 -0
  70. polaris/plugin/service_router/metadata.py +84 -0
  71. polaris/plugin/service_router/recover.py +62 -0
  72. polaris/plugin/service_router/util.py +54 -0
  73. polaris/plugin/skill/__init__.py +16 -0
  74. polaris/plugin/skill/connector.py +127 -0
  75. polaris/plugin/skill/persistence.py +94 -0
  76. polaris/plugin/stat_reporter/__init__.py +16 -0
  77. polaris/plugin/stat_reporter/base.py +34 -0
  78. polaris/plugin/stat_reporter/prometheus.py +375 -0
  79. polaris/plugin_manager.py +146 -0
  80. polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
  81. polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
  82. polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
  83. polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
  84. polaris_python-0.0.1.dev3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,92 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ from polaris.pkg.model.core import ServiceKey
17
+
18
+
19
+ class DefaultConf:
20
+ HomeVar = "$HOME"
21
+ DefaultServerConnector = "grpc"
22
+ DefaultLocalCache = "inmemory"
23
+ DefaultServiceRouterIsolated = "isolatedRouter"
24
+ DefaultServiceRouterMetadata = "metadataRouter"
25
+ DefaultServiceRouterRecover = "recoverRouter"
26
+ DefaultLoadBalancer = "weightedRandom"
27
+
28
+ DefaultAPIInvokeTimeout = 1 * 1000
29
+ DefaultAPIMaxRetryTimes = 1
30
+ DefaultAPIRetryInterval = 500
31
+ DefaultDiscoverServiceRetryInterval = 5 * 1000
32
+ DefaultServiceExpireTime = 24 * 60 * 60 * 1000
33
+ DefaultServerConnectTimeout = 500
34
+ DefaultServerConnectionIdleTimeout = 60 * 1000
35
+ DefaultBuiltInServerConnectionCloseTimeout = 2 * DefaultServerConnectionIdleTimeout
36
+ DefaultRequestQueueSize = 1000
37
+ DefaultServerSwitchInterval = 10 * 60 * 1000
38
+ DefaultAgentListenAddress = "127.0.0.1:8888"
39
+ DefaultCachePersistDir = "./polaris/backup"
40
+ DefaultPersistMaxWriteRetry = 1
41
+ DefaultPersistMaxReadRetry = 0
42
+ DefaultPersistRetryInterval = 500
43
+ DefaultCircuitBreakerCheckPeriod = 60 * 1000
44
+ MinCircuitBreakerCheckPeriod = 1 * 1000
45
+ DefaultCircuitBreakerEnabled = True
46
+ DefaultRequestCountAfterHalfOpen = 10
47
+ DefaultSuccessCountAfterHalfOpen = 3
48
+ DefaultSleepWindow = 30 * 1000
49
+ MinSleepWindow = 1 * 1000
50
+ DefaultRuleEnable = True
51
+ DefaultErrorCount = 10
52
+ DefaultErrorPercent = 50
53
+ DefaultInterval = 60 * 1000
54
+ DefaultMinimumRequest = 10
55
+ DefaultPercentOfMinInstances = 0.0
56
+ DefaultReportClientIntervalDuration = 60 * 1000
57
+ MaxRedirectTimes = 1
58
+ DefaultServiceRefreshIntervalDuration = 2 * 1000
59
+ DefaultStatReportEnabled = True
60
+ DefaultStatReporter = "prometheus"
61
+ DefaultBuiltInServer = ["127.0.0.1:8091"]
62
+ ServerNamespace = "Polaris"
63
+ ServerDiscoverService = "polaris.discover"
64
+ ServerHeartBeatService = "polaris.healthcheck"
65
+ ServerMonitorService = "polaris.monitor"
66
+ SvcKeyDiscoverService = ServiceKey(ServerNamespace, ServerDiscoverService)
67
+ SvcKeyHeartBeatService = ServiceKey(ServerNamespace, ServerHeartBeatService)
68
+ SvcKeyMonitorService = ServiceKey(ServerNamespace, ServerMonitorService)
69
+ DefaultLoadBalancerWR = "weightedRandom"
70
+ DefaultServerMessageTimeout = 5 * 1000
71
+
72
+ BuiltinCluster = "builtin"
73
+ DiscoverCluster = "discover"
74
+ HealthCheckCluster = "healthCheck"
75
+ MonitorCluster = "monitor"
76
+
77
+ DefaultServerServiceToLoadBalancer = {
78
+ BuiltinCluster: DefaultLoadBalancerWR,
79
+ DiscoverCluster: DefaultLoadBalancerWR,
80
+ HealthCheckCluster: DefaultLoadBalancerWR,
81
+ MonitorCluster: DefaultLoadBalancerWR
82
+ }
83
+
84
+ # System services use the global before/after chains; only override the core chain.
85
+ DefaultServerServiceRouterChain = []
86
+ DefaultCircuitBreaker = "composite"
87
+ DefaultCircuitBreakerErrCount = "errorCount"
88
+ DefaultCircuitBreakerErrRate = "errorRate"
89
+
90
+ DefaultMinServiceExpireTime = 5 * 1000 # 5 seconds
91
+ DefaultMinTimingInterval = 0.1 * 1000 # 100 ms
92
+ DefaultServerServiceRefreshInterval = 60 * 1000 # 1 minute
@@ -0,0 +1,76 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """Public configuration for the Java/Go-compatible event reporter."""
18
+
19
+ from dataclasses import dataclass, field
20
+ from urllib.parse import urlsplit
21
+
22
+ from polaris.pkg.model.error import ErrorCode, SDKError
23
+
24
+
25
+ @dataclass
26
+ class PushGatewayEventConfig:
27
+ """Configure batching and addressing for the event Pushgateway reporter."""
28
+
29
+ address: list[str] = field(default_factory=list)
30
+ event_queue_size: int = 1000
31
+ max_batch_size: int = 100
32
+ namespace: str = "Polaris"
33
+ service: str = "polaris.pushgateway"
34
+ report_path: str = "/polaris/client/events"
35
+ push_interval: int = 1000
36
+ max_retries: int = 3
37
+ retry_interval: int = 1000
38
+ override_host: str = ""
39
+
40
+ def verify(self):
41
+ for key in ("address",):
42
+ value = getattr(self, key)
43
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
44
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"pushgateway.{key} must be a list of strings")
45
+ for key in ("event_queue_size", "max_batch_size", "push_interval", "max_retries"):
46
+ value = getattr(self, key)
47
+ if type(value) is not int or value <= 0:
48
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"pushgateway.{key} must be positive")
49
+ if type(self.retry_interval) is not int or self.retry_interval < 0:
50
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "pushgateway.retryInterval must be >=0")
51
+ for key in ("namespace", "service", "report_path"):
52
+ if not isinstance(getattr(self, key), str) or not getattr(self, key).strip():
53
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"pushgateway.{key} must be nonempty")
54
+ if not isinstance(self.override_host, str):
55
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "pushgateway.overrideHost must be a string")
56
+ if not self.report_path.startswith("/") or "?" in self.report_path or "#" in self.report_path:
57
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "pushgateway.reportPath must be an absolute path")
58
+ for address in self.address:
59
+ try:
60
+ url = urlsplit(address if "://" in address else f"http://{address}")
61
+ if (url.scheme not in ("http", "https") or not url.hostname or not url.port
62
+ or url.username or url.password or url.query or url.fragment):
63
+ raise ValueError("require http(s) host:port without credentials, query or fragment")
64
+ except ValueError as error:
65
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"Invalid pushgateway.address: {error}") from error
66
+
67
+
68
+ def pushgateway_event_config(value):
69
+ """Parse YAML plugin mappings or capture directly assigned configuration objects."""
70
+ if isinstance(value, PushGatewayEventConfig):
71
+ result = value
72
+ else:
73
+ from polaris.pkg.config.loader import _build_object
74
+ result = _build_object(PushGatewayEventConfig, value or {}, "global.eventReporter.plugin.pushgateway")
75
+ result.verify()
76
+ return result
@@ -0,0 +1,392 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """Load Configuration objects from YAML and environment variables."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import copy
22
+ import inspect
23
+ import os
24
+ import re
25
+ from decimal import Decimal, InvalidOperation
26
+ from collections.abc import Mapping
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ import yaml
31
+ from polaris.pkg.config.stat_reporter import AdminConfig, PrometheusConfig
32
+ from polaris.pkg.config.event import PushGatewayEventConfig
33
+
34
+ from polaris.pkg.config.api import (
35
+ AIConfig,
36
+ SkillConfig,
37
+ SkillConnectorConfig,
38
+ APIConfig,
39
+ CircuitBreakerConfig,
40
+ Configuration,
41
+ ConsumerConfig,
42
+ GlobalConfig,
43
+ LoadBalancerConfig,
44
+ LocalCacheConfig,
45
+ LosslessConfig,
46
+ ProviderConfig,
47
+ ServerClusterConfig,
48
+ ServerConnectorConfig,
49
+ ServiceRouterConfig,
50
+ StatReporterConfig,
51
+ SystemConfig,
52
+ EventReporterConfig,
53
+ WeightAdjustConfig,
54
+ )
55
+ from polaris.pkg.model.error import ErrorCode, SDKError
56
+
57
+ DEFAULT_CONFIG_FILE_NAMES = ("polaris.yaml",)
58
+ ENV_CONFIG_FILE = "POLARIS_CONFIG_FILE"
59
+
60
+ _CAMEL_CASE = re.compile(r"(?<!^)(?=[A-Z])")
61
+ _ENV_VARIABLE = re.compile(
62
+ r"\$\{(?P<braced>[A-Za-z_][A-Za-z0-9_]*)(?::(?P<default>[^}]*))?\}"
63
+ r"|\$(?P<bare>[A-Za-z_][A-Za-z0-9_]*)"
64
+ )
65
+ _DURATION_PART = re.compile(r"(\d+(?:\.\d+)?)(ns|us|ms|s|m|h)")
66
+ _DURATION_UNITS = {
67
+ "ns": Decimal("0.000001"), "us": Decimal("0.001"), "ms": Decimal(1),
68
+ "s": Decimal(1000), "m": Decimal(60000), "h": Decimal(3600000),
69
+ }
70
+
71
+ _FIELD_ALIASES = {
72
+ Configuration: {"global": "global_config", "consumer": "consumer_config", "provider": "provider_config"},
73
+ GlobalConfig: {
74
+ "system": "system_conf",
75
+ "api": "api_conf",
76
+ "server_connector": "server_connector",
77
+ "serverconnector": "server_connector",
78
+ "stat_reporter": "stat_reporter",
79
+ "statreporter": "stat_reporter",
80
+ "event_reporter": "event_reporter",
81
+ "eventreporter": "event_reporter",
82
+ },
83
+ ConsumerConfig: {
84
+ "load_balance": "load_balance",
85
+ "loadbalancer": "load_balance",
86
+ "load_balancer": "load_balance",
87
+ },
88
+ APIConfig: {"bind_i_p": "bind_ip", "bind_if": "bind_intf"},
89
+ ServerConnectorConfig: {"message_timeout": "message_time", "trusted_c_a_file": "trusted_ca_file"},
90
+ SkillConnectorConfig: {"trusted_c_a_file": "trusted_ca_file"},
91
+ }
92
+
93
+ _NESTED_TYPES = {
94
+ Configuration: {
95
+ "global_config": GlobalConfig,
96
+ "consumer_config": ConsumerConfig,
97
+ "provider_config": ProviderConfig,
98
+ "ai": AIConfig,
99
+ },
100
+ GlobalConfig: {
101
+ "system_conf": SystemConfig,
102
+ "api_conf": APIConfig,
103
+ "server_connector": ServerConnectorConfig,
104
+ "stat_reporter": StatReporterConfig,
105
+ "admin": AdminConfig,
106
+ "event_reporter": EventReporterConfig,
107
+ },
108
+ SystemConfig: {
109
+ "discover_cluster": ServerClusterConfig,
110
+ "health_check_cluster": ServerClusterConfig,
111
+ "monitor_cluster": ServerClusterConfig,
112
+ },
113
+ ConsumerConfig: {
114
+ "local_cache": LocalCacheConfig,
115
+ "service_router": ServiceRouterConfig,
116
+ "load_balance": LoadBalancerConfig,
117
+ "circuit_breaker": CircuitBreakerConfig,
118
+ "weight_adjust": WeightAdjustConfig,
119
+ },
120
+ ProviderConfig: {"lossless": LosslessConfig},
121
+ AIConfig: {"skill": SkillConfig},
122
+ SkillConfig: {"server_connector": SkillConnectorConfig},
123
+ }
124
+
125
+ _DURATION_FIELDS = {
126
+ PrometheusConfig: {"push_interval"},
127
+ PushGatewayEventConfig: {"push_interval", "retry_interval"},
128
+ ProviderConfig: {"min_register_interval"},
129
+ LosslessConfig: {"delay_register_interval", "health_check_interval"},
130
+ ServerClusterConfig: {"refresh_interval"},
131
+ APIConfig: {"timeout", "report_interval", "retry_interval"},
132
+ ServerConnectorConfig: {
133
+ "connect_timeout", "message_time", "connection_idle_timeout", "server_switch_interval",
134
+ },
135
+ LocalCacheConfig: {"service_expire_time", "service_refresh_interval", "persist_retry_interval"},
136
+ CircuitBreakerConfig: {"check_period", "sleep_window", "default_interval"},
137
+ SkillConnectorConfig: {"message_timeout", "download_timeout", "persist_retry_interval"},
138
+ }
139
+
140
+
141
+ def _snake_case(name: str) -> str:
142
+ return _CAMEL_CASE.sub("_", name.replace("-", "_")).lower()
143
+
144
+
145
+ def _normalise_keys(cls: type, data: Mapping[str, Any]) -> dict[str, Any]:
146
+ if not isinstance(data, Mapping):
147
+ raise SDKError(
148
+ ErrorCode.ErrCodeAPIInvalidConfig,
149
+ f"Configuration section for {cls.__name__} must be an object",
150
+ )
151
+
152
+ aliases = _FIELD_ALIASES.get(cls, {})
153
+ result: dict[str, Any] = {}
154
+ for key, value in data.items():
155
+ normalised = _snake_case(str(key))
156
+ field = aliases.get(normalised, normalised)
157
+ if field in aliases and aliases[field] != field:
158
+ field = aliases[field]
159
+ result[field] = value
160
+ return result
161
+
162
+
163
+ def _normalise_sections(cls: type, data: Mapping[str, Any]) -> dict[str, Any]:
164
+ values = _normalise_keys(cls, data)
165
+ for key, nested_type in _NESTED_TYPES.get(cls, {}).items():
166
+ if isinstance(values.get(key), Mapping):
167
+ values[key] = _normalise_sections(nested_type, values[key])
168
+ return values
169
+
170
+
171
+ def _duration_ms(value: Any, path: str) -> int:
172
+ if isinstance(value, int) and not isinstance(value, bool):
173
+ return value
174
+ if isinstance(value, str):
175
+ text = value.strip()
176
+ if re.fullmatch(r"[+-]?\d+", text):
177
+ return int(text)
178
+ sign = -1 if text.startswith("-") else 1
179
+ remaining = text[1:] if text.startswith(("+", "-")) else text
180
+ total = Decimal(0)
181
+ matched = False
182
+ try:
183
+ while remaining:
184
+ match = _DURATION_PART.match(remaining)
185
+ if match is None:
186
+ break
187
+ matched = True
188
+ total += Decimal(match[1]) * _DURATION_UNITS[match[2]]
189
+ remaining = remaining[match.end():]
190
+ if matched and not remaining and total == total.to_integral_value():
191
+ return sign * int(total)
192
+ except InvalidOperation:
193
+ pass
194
+ raise SDKError(
195
+ ErrorCode.ErrCodeAPIInvalidConfig,
196
+ f"{path} must be integer milliseconds or a duration such as 1000ms or 1s",
197
+ )
198
+
199
+
200
+ def _expand_environment(value: Any, env: Mapping[str, str]) -> Any:
201
+ if isinstance(value, str):
202
+ def replace(match: re.Match) -> str:
203
+ braced = match.group("braced")
204
+ if braced is not None:
205
+ replacement = env.get(braced)
206
+ if replacement:
207
+ return replacement
208
+ return match.group("default") or ""
209
+ return env.get(match.group("bare"), "")
210
+
211
+ return _ENV_VARIABLE.sub(replace, value)
212
+ if isinstance(value, Mapping):
213
+ return {key: _expand_environment(child, env) for key, child in value.items()}
214
+ if isinstance(value, list):
215
+ return [_expand_environment(child, env) for child in value]
216
+ return value
217
+
218
+
219
+ def _build_object(cls: type, data: Mapping[str, Any], path: str) -> Any:
220
+ values = _normalise_keys(cls, data)
221
+ signature = inspect.signature(cls.__init__).parameters
222
+ accepted = set(signature) - {"self"}
223
+ unknown = sorted(set(values) - accepted)
224
+ if unknown:
225
+ raise SDKError(
226
+ ErrorCode.ErrCodeAPIInvalidConfig,
227
+ f"Unknown configuration keys at {path}: {', '.join(unknown)}",
228
+ )
229
+
230
+ nested_types = _NESTED_TYPES.get(cls, {})
231
+ kwargs: dict[str, Any] = {}
232
+ for key, value in values.items():
233
+ nested_type = nested_types.get(key)
234
+ if nested_type is not None:
235
+ kwargs[key] = None if value is None else _build_object(nested_type, value, f"{path}.{key}")
236
+ elif key in _DURATION_FIELDS.get(cls, set()):
237
+ kwargs[key] = _duration_ms(value, f"{path}.{key}")
238
+ else:
239
+ kwargs[key] = value
240
+
241
+ try:
242
+ return cls(**kwargs)
243
+ except TypeError as exc:
244
+ raise SDKError(
245
+ ErrorCode.ErrCodeAPIInvalidConfig,
246
+ f"Invalid configuration at {path}: {exc}",
247
+ ) from exc
248
+
249
+
250
+ def parse_config_stream(buf: str | bytes, source: str = "config stream") -> dict[str, Any]:
251
+ """Parse a YAML configuration document."""
252
+ try:
253
+ text = buf.decode("utf-8") if isinstance(buf, bytes) else buf
254
+ if not isinstance(text, str):
255
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"{source} must be YAML text or bytes")
256
+ data = yaml.safe_load(text)
257
+ except (yaml.YAMLError, UnicodeDecodeError) as exc:
258
+ raise SDKError(
259
+ ErrorCode.ErrCodeAPIInvalidConfig,
260
+ f"Failed to parse {source}: {exc}",
261
+ ) from exc
262
+
263
+ if data is None:
264
+ return {}
265
+ if not isinstance(data, dict):
266
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"{source} must contain a configuration object")
267
+ return data
268
+
269
+
270
+ def load_config_file(path: str | Path) -> dict[str, Any]:
271
+ config_path = Path(path).expanduser()
272
+ if not config_path.is_file():
273
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"Configuration file not found: {config_path}")
274
+ if config_path.suffix.lower() != ".yaml":
275
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "Only .yaml configuration files are supported")
276
+ try:
277
+ return parse_config_stream(config_path.read_bytes(), str(config_path))
278
+ except OSError as exc:
279
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"Failed to read configuration file: {config_path}") from exc
280
+
281
+
282
+ def _set_nested(data: dict[str, Any], keys: tuple[str, ...], value: Any) -> None:
283
+ target = data
284
+ for key in keys[:-1]:
285
+ child = target.get(key)
286
+ if not isinstance(child, dict):
287
+ child = {}
288
+ target[key] = child
289
+ target = child
290
+ target[keys[-1]] = value
291
+
292
+
293
+ def _as_bool(value: str) -> bool:
294
+ lowered = value.strip().lower()
295
+ if lowered in {"1", "true", "yes", "on"}:
296
+ return True
297
+ if lowered in {"0", "false", "no", "off"}:
298
+ return False
299
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"Invalid boolean environment value: {value}")
300
+
301
+
302
+ def _as_int(value: str, name: str) -> int:
303
+ try:
304
+ return int(value)
305
+ except ValueError as exc:
306
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"{name} must be an integer") from exc
307
+
308
+
309
+ def _as_list(value: str) -> list[str]:
310
+ return [item.strip() for item in value.split(",") if item.strip()]
311
+
312
+
313
+ def apply_environment(data: Mapping[str, Any] | None = None,
314
+ environ: Mapping[str, str] | None = None) -> dict[str, Any]:
315
+ """Overlay POLARIS_ environment variables on a configuration mapping."""
316
+ # Merge overrides into the same section regardless of its key spelling.
317
+ result = _normalise_sections(Configuration, copy.deepcopy(dict(data or {})))
318
+ env = os.environ if environ is None else environ
319
+ global_config = result.get("global_config")
320
+ connector = global_config.get("server_connector", {}) if isinstance(global_config, Mapping) else {}
321
+ addresses = connector.get("addresses", []) if isinstance(connector, Mapping) else []
322
+ server_is_placeholder = isinstance(addresses, list) and any(
323
+ isinstance(address, str) and any(
324
+ (match.group("braced") or match.group("bare")) == "POLARIS_SERVER"
325
+ for match in _ENV_VARIABLE.finditer(address)
326
+ )
327
+ for address in addresses
328
+ )
329
+ result = _expand_environment(result, env)
330
+
331
+ def value(name: str) -> str | None:
332
+ return env.get(name)
333
+
334
+ server_address = value("POLARIS_SERVER_ADDRESSES") or value("POLARIS_SERVER_ADDRESS")
335
+ if not server_address and not server_is_placeholder:
336
+ server_address = value("POLARIS_SERVER")
337
+ if server_address:
338
+ _set_nested(result, ("global_config", "server_connector", "addresses"), _as_list(server_address))
339
+ if (token := value("POLARIS_TOKEN")) is not None:
340
+ _set_nested(result, ("global_config", "server_connector", "token"), token)
341
+ if protocol := value("POLARIS_PROTOCOL"):
342
+ _set_nested(result, ("global_config", "server_connector", "protocol"), protocol)
343
+
344
+ timeout = value("POLARIS_API_TIMEOUT") or value("POLARIS_TIMEOUT")
345
+ if timeout:
346
+ _set_nested(result, ("global_config", "api_conf", "timeout"), _as_int(timeout, "POLARIS_TIMEOUT"))
347
+ retries = value("POLARIS_API_MAX_RETRY_TIMES") or value("POLARIS_MAX_RETRY_TIMES")
348
+ if retries:
349
+ _set_nested(result, ("global_config", "api_conf", "max_retry_times"), _as_int(retries, "POLARIS_MAX_RETRY_TIMES"))
350
+ retry_interval = value("POLARIS_API_RETRY_INTERVAL") or value("POLARIS_RETRY_INTERVAL")
351
+ if retry_interval:
352
+ _set_nested(result, ("global_config", "api_conf", "retry_interval"),
353
+ _as_int(retry_interval, "POLARIS_RETRY_INTERVAL"))
354
+
355
+ persist_dir = value("POLARIS_LOCAL_CACHE_PERSIST_DIR") or value("POLARIS_CACHE_PERSIST_DIR")
356
+ if persist_dir:
357
+ _set_nested(result, ("consumer_config", "local_cache", "persist_dir"), persist_dir)
358
+ refresh_interval = value("POLARIS_SERVICE_REFRESH_INTERVAL")
359
+ if refresh_interval:
360
+ _set_nested(result, ("consumer_config", "local_cache", "service_refresh_interval"),
361
+ _as_int(refresh_interval, "POLARIS_SERVICE_REFRESH_INTERVAL"))
362
+ expire_time = value("POLARIS_SERVICE_EXPIRE_TIME")
363
+ if expire_time:
364
+ _set_nested(result, ("consumer_config", "local_cache", "service_expire_time"),
365
+ _as_int(expire_time, "POLARIS_SERVICE_EXPIRE_TIME"))
366
+
367
+ stat_enable = value("POLARIS_STAT_REPORTER_ENABLE")
368
+ if stat_enable:
369
+ _set_nested(result, ("global_config", "stat_reporter", "enable"), _as_bool(stat_enable))
370
+ stat_chain = value("POLARIS_STAT_REPORTER_CHAIN")
371
+ if stat_chain:
372
+ _set_nested(result, ("global_config", "stat_reporter", "chain"), _as_list(stat_chain))
373
+ return result
374
+
375
+
376
+ def build_configuration(data: Mapping[str, Any], apply_env: bool = True,
377
+ environ: Mapping[str, str] | None = None) -> Configuration:
378
+ values = apply_environment(data, environ) if apply_env else dict(data)
379
+ return _build_object(Configuration, values, "configuration")
380
+
381
+
382
+ def find_default_config_file(environ: Mapping[str, str] | None = None) -> Path | None:
383
+ env = os.environ if environ is None else environ
384
+ if configured := env.get(ENV_CONFIG_FILE):
385
+ return Path(configured).expanduser()
386
+
387
+ candidates = [Path.cwd() / name for name in DEFAULT_CONFIG_FILE_NAMES]
388
+ candidates.extend(Path.home() / ".config" / "polaris" / name for name in DEFAULT_CONFIG_FILE_NAMES)
389
+ for candidate in candidates:
390
+ if candidate.is_file():
391
+ return candidate
392
+ return None
@@ -0,0 +1,100 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+
16
+ """Public configuration shared with the Java Prometheus reporter."""
17
+
18
+ from dataclasses import dataclass, field
19
+ import re
20
+ from urllib.parse import urlsplit
21
+
22
+ from polaris.pkg.model.error import ErrorCode, SDKError
23
+
24
+ DEFAULT_ADMIN_PORT = 28080
25
+
26
+
27
+ class AdminConfig:
28
+ """Assign the SDK metrics HTTP listener address."""
29
+
30
+ def __init__(self, host=None, port=None):
31
+ self.host = host
32
+ self.port = port
33
+
34
+ def set_default(self):
35
+ if self.host is None:
36
+ self.host = "0.0.0.0"
37
+ if self.port is None:
38
+ self.port = DEFAULT_ADMIN_PORT
39
+
40
+ def verify(self):
41
+ if not isinstance(self.host, str) or not self.host.strip():
42
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "global.admin.host must be nonempty")
43
+ if type(self.port) is not int or not -1 <= self.port <= 65535:
44
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "global.admin.port must be -1..65535")
45
+
46
+
47
+ @dataclass
48
+ class PrometheusConfig:
49
+ """Assign pull or push export and Java-compatible method aggregation."""
50
+
51
+ type: str = "pull"
52
+ path: str = "/metric"
53
+ address: list[str] = field(default_factory=list)
54
+ namespace: str = "Polaris"
55
+ service: str = "polaris.pushgateway"
56
+ push_interval: int = 10000
57
+ open_gzip: bool = False
58
+ path_regex_list: list[str] = field(default_factory=list)
59
+ override_host: str = ""
60
+
61
+ def verify(self):
62
+ if self.type not in ("pull", "push"):
63
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "prometheus.type must be pull or push")
64
+ if not isinstance(self.path, str) or not self.path.startswith("/") or "?" in self.path:
65
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "prometheus.path must be an absolute HTTP path")
66
+ if type(self.push_interval) is not int or self.push_interval < 1000:
67
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "prometheus.pushInterval must be >= 1000ms")
68
+ if type(self.open_gzip) is not bool:
69
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, "prometheus.openGzip must be boolean")
70
+ for key in ("address", "path_regex_list"):
71
+ value = getattr(self, key)
72
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
73
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"prometheus.{key} must be a list of strings")
74
+ for key in ("namespace", "service", "override_host"):
75
+ if not isinstance(getattr(self, key), str):
76
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"prometheus.{key} must be a string")
77
+ for address in self.address:
78
+ try:
79
+ url = urlsplit(address if "://" in address else f"http://{address}")
80
+ if (url.scheme not in ("http", "https") or not url.hostname or not url.port
81
+ or url.username or url.password or url.query or url.fragment):
82
+ raise ValueError("require http(s) host:port without credentials, query or fragment")
83
+ except ValueError as error:
84
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"Invalid prometheus.address: {error}") from error
85
+ try:
86
+ for pattern in self.path_regex_list:
87
+ re.compile(pattern)
88
+ except re.error as error:
89
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig, f"Invalid prometheus pathRegexList: {error}") from error
90
+
91
+
92
+ def prometheus_config(value):
93
+ """Parse YAML plugin mappings or capture directly assigned configuration objects."""
94
+ if isinstance(value, PrometheusConfig):
95
+ result = value
96
+ else:
97
+ from polaris.pkg.config.loader import _build_object
98
+ result = _build_object(PrometheusConfig, value or {}, "global.statReporter.plugin.prometheus")
99
+ result.verify()
100
+ return result
@@ -0,0 +1,15 @@
1
+ # Tencent is pleased to support the open source community by making polaris-python available.
2
+ #
3
+ # Copyright (C) 2021 Tencent. All rights reserved.
4
+ #
5
+ # Licensed under the BSD 3-Clause License (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://opensource.org/licenses/BSD-3-Clause
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software distributed
12
+ # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13
+ # CONDITIONS OF ANY KIND, either express or implied. See the License for the
14
+ # specific language governing permissions and limitations under the License.
15
+