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.
- polaris/__init__.py +74 -0
- polaris/_version.py +16 -0
- polaris/api/__init__.py +55 -0
- polaris/api/_log_file.py +232 -0
- polaris/api/config.py +145 -0
- polaris/api/consumer.py +240 -0
- polaris/api/log.py +414 -0
- polaris/api/lossless.py +73 -0
- polaris/api/provider.py +152 -0
- polaris/api/skill.py +162 -0
- polaris/pkg/__init__.py +15 -0
- polaris/pkg/common/__init__.py +15 -0
- polaris/pkg/common/plugin.py +33 -0
- polaris/pkg/config/__init__.py +15 -0
- polaris/pkg/config/api.py +1205 -0
- polaris/pkg/config/default.py +92 -0
- polaris/pkg/config/event.py +76 -0
- polaris/pkg/config/loader.py +392 -0
- polaris/pkg/config/stat_reporter.py +100 -0
- polaris/pkg/flow/__init__.py +15 -0
- polaris/pkg/flow/admin_server.py +104 -0
- polaris/pkg/flow/async_flow.py +55 -0
- polaris/pkg/flow/base_flow.py +111 -0
- polaris/pkg/flow/impl.py +1277 -0
- polaris/pkg/flow/lossless.py +605 -0
- polaris/pkg/flow/notify.py +46 -0
- polaris/pkg/flow/register.py +210 -0
- polaris/pkg/flow/skill.py +110 -0
- polaris/pkg/flow/sync_flow.py +20 -0
- polaris/pkg/model/__init__.py +15 -0
- polaris/pkg/model/context.py +53 -0
- polaris/pkg/model/contract.py +89 -0
- polaris/pkg/model/core.py +51 -0
- polaris/pkg/model/engine.py +83 -0
- polaris/pkg/model/error.py +94 -0
- polaris/pkg/model/event.py +159 -0
- polaris/pkg/model/lossless.py +102 -0
- polaris/pkg/model/rule.py +152 -0
- polaris/pkg/model/server.py +288 -0
- polaris/pkg/model/service.py +820 -0
- polaris/pkg/model/skill.py +179 -0
- polaris/pkg/model/stat.py +168 -0
- polaris/pkg/model/util.py +58 -0
- polaris/pkg/version/__init__.py +18 -0
- polaris/plugin/__init__.py +32 -0
- polaris/plugin/circuit_breaker/__init__.py +19 -0
- polaris/plugin/circuit_breaker/base.py +54 -0
- polaris/plugin/circuit_breaker/composite.py +265 -0
- polaris/plugin/circuit_breaker/config.py +36 -0
- polaris/plugin/event/__init__.py +21 -0
- polaris/plugin/event/pushgateway.py +202 -0
- polaris/plugin/load_balancer/__init__.py +24 -0
- polaris/plugin/load_balancer/base.py +30 -0
- polaris/plugin/load_balancer/random.py +39 -0
- polaris/plugin/load_balancer/weighted_random.py +50 -0
- polaris/plugin/local_registry/__init__.py +18 -0
- polaris/plugin/local_registry/base.py +47 -0
- polaris/plugin/local_registry/file_cache.py +335 -0
- polaris/plugin/local_registry/local_cache.py +433 -0
- polaris/plugin/server_connector/__init__.py +20 -0
- polaris/plugin/server_connector/base.py +104 -0
- polaris/plugin/server_connector/convert.py +149 -0
- polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
- polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
- polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
- polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
- polaris/plugin/service_router/__init__.py +28 -0
- polaris/plugin/service_router/base.py +105 -0
- polaris/plugin/service_router/isolated.py +35 -0
- polaris/plugin/service_router/metadata.py +84 -0
- polaris/plugin/service_router/recover.py +62 -0
- polaris/plugin/service_router/util.py +54 -0
- polaris/plugin/skill/__init__.py +16 -0
- polaris/plugin/skill/connector.py +127 -0
- polaris/plugin/skill/persistence.py +94 -0
- polaris/plugin/stat_reporter/__init__.py +16 -0
- polaris/plugin/stat_reporter/base.py +34 -0
- polaris/plugin/stat_reporter/prometheus.py +375 -0
- polaris/plugin_manager.py +146 -0
- polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
- polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
- polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
- polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
- 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
|
+
|