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
polaris/__init__.py ADDED
@@ -0,0 +1,74 @@
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.api.config import (
17
+ SDKContext,
18
+ init_context,
19
+ init_context_by_config,
20
+ init_context_by_file,
21
+ init_context_by_stream,
22
+ )
23
+ from polaris.api.consumer import ConsumerAPI, ServiceWatch, create_consumer_by_config, create_consumer_by_file
24
+ from polaris.api.log import LogOptions
25
+ from polaris.api.lossless import LosslessAPI
26
+ from polaris.api.provider import ProviderAPI, create_provider_by_config, create_provider_by_file
27
+ from polaris.api.skill import (
28
+ SkillAPI,
29
+ create_skill_by_config,
30
+ create_skill_by_context,
31
+ create_skill_by_default_config_file,
32
+ create_skill_by_file,
33
+ )
34
+ from polaris.pkg.model.service import Instance
35
+ from polaris.pkg.model.event import EventCenter, SDKEvent, SDKEventName, SDKEventType
36
+ from polaris.pkg.model.lossless import InstanceProperties, LosslessActionProvider, LosslessInstance, RegisterStatus
37
+ from polaris.pkg.config.api import Configuration
38
+ from polaris.pkg.model.error import ErrorCode, SDKError
39
+ from polaris._version import __version__
40
+
41
+ __all__ = [
42
+ "Configuration",
43
+ "ConsumerAPI",
44
+ "ServiceWatch",
45
+ "ErrorCode",
46
+ "LogOptions",
47
+ "LosslessAPI",
48
+ "LosslessActionProvider",
49
+ "LosslessInstance",
50
+ "InstanceProperties",
51
+ "ProviderAPI",
52
+ "Instance",
53
+ "EventCenter",
54
+ "SDKEvent",
55
+ "SDKEventName",
56
+ "SDKEventType",
57
+ "SkillAPI",
58
+ "RegisterStatus",
59
+ "create_skill_by_config",
60
+ "create_skill_by_context",
61
+ "create_skill_by_default_config_file",
62
+ "create_skill_by_file",
63
+ "SDKContext",
64
+ "SDKError",
65
+ "create_consumer_by_config",
66
+ "create_consumer_by_file",
67
+ "create_provider_by_config",
68
+ "create_provider_by_file",
69
+ "init_context_by_config",
70
+ "init_context_by_file",
71
+ "init_context",
72
+ "init_context_by_stream",
73
+ "__version__",
74
+ ]
polaris/_version.py ADDED
@@ -0,0 +1,16 @@
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
+ __version__ = "0.0.1.dev3"
@@ -0,0 +1,55 @@
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 Polaris SDK APIs."""
17
+
18
+ from polaris.api.config import (
19
+ SDKContext,
20
+ init_context,
21
+ init_context_by_config,
22
+ init_context_by_file,
23
+ init_context_by_stream,
24
+ )
25
+ from polaris.api.consumer import ConsumerAPI, ServiceWatch, create_consumer_by_config, create_consumer_by_file
26
+ from polaris.api.log import LogOptions
27
+ from polaris.api.provider import ProviderAPI, create_provider_by_config, create_provider_by_file
28
+ from polaris.api.skill import (
29
+ SkillAPI,
30
+ create_skill_by_config,
31
+ create_skill_by_context,
32
+ create_skill_by_default_config_file,
33
+ create_skill_by_file,
34
+ )
35
+
36
+ __all__ = [
37
+ "ConsumerAPI",
38
+ "ServiceWatch",
39
+ "LogOptions",
40
+ "ProviderAPI",
41
+ "SkillAPI",
42
+ "SDKContext",
43
+ "create_consumer_by_config",
44
+ "create_consumer_by_file",
45
+ "create_provider_by_config",
46
+ "create_provider_by_file",
47
+ "create_skill_by_config",
48
+ "create_skill_by_context",
49
+ "create_skill_by_default_config_file",
50
+ "create_skill_by_file",
51
+ "init_context_by_config",
52
+ "init_context_by_file",
53
+ "init_context",
54
+ "init_context_by_stream",
55
+ ]
@@ -0,0 +1,232 @@
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
+ """Exclusive file ownership for SDK rotating logs."""
17
+
18
+ import errno
19
+ import logging
20
+ import os
21
+ import re
22
+ import stat
23
+ import time
24
+ from logging.handlers import RotatingFileHandler
25
+ from pathlib import Path
26
+ from threading import Event, RLock, Thread
27
+
28
+ try:
29
+ import fcntl
30
+ except ImportError: # pragma: no cover - Windows CI
31
+ fcntl = None
32
+ import msvcrt
33
+
34
+ from polaris.pkg.model.error import ErrorCode, SDKError
35
+
36
+ DEFAULT_MAX_BYTES = 50 * 1024 * 1024
37
+ DEFAULT_BACKUP_COUNT = 5
38
+ DEFAULT_MAX_AGE_DAYS = 30
39
+ CLEANUP_INTERVAL_SECONDS = 3600
40
+ _UUID_PATTERN = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
41
+ _maintenance_logger = logging.getLogger("polaris.log.maintenance")
42
+
43
+
44
+ class LogFileLock:
45
+ """Exclusively own a base file and its numbered backup namespace."""
46
+
47
+ def __init__(self, path):
48
+ self.path = Path(path).resolve()
49
+ self.stream = None
50
+ self.path.parent.mkdir(parents=True, exist_ok=True)
51
+ lock_path = self.path
52
+ # Explicit numbered filenames must not overlap another logger's backups.
53
+ while re.fullmatch(r"\.[1-9][0-9]*", lock_path.suffix):
54
+ lock_path = lock_path.with_suffix("")
55
+ stream = lock_path.with_name(f".{lock_path.name}.lock").open("a+b")
56
+ try:
57
+ if fcntl is not None:
58
+ fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
59
+ else: # pragma: no cover - Windows CI
60
+ if os.fstat(stream.fileno()).st_size == 0:
61
+ stream.write(b"\0")
62
+ stream.flush()
63
+ stream.seek(0)
64
+ msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1)
65
+ except OSError as error:
66
+ stream.close()
67
+ if error.errno in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
68
+ raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig,
69
+ f"Log file is already in use: {self.path}") from error
70
+ raise
71
+ self.stream = stream
72
+
73
+ def close(self):
74
+ """Release ownership without unlinking the lock inode used by other processes."""
75
+ if self.stream is not None:
76
+ self.stream.close()
77
+ self.stream = None
78
+
79
+
80
+ def reserve_log_files(paths, retained=None):
81
+ """Reserve all category paths atomically, retaining a process logger's own locks."""
82
+ retained = retained or {}
83
+ locks = {}
84
+ acquired = []
85
+ try:
86
+ for path in sorted(set(paths)):
87
+ lock = retained.get(path)
88
+ if lock is None:
89
+ lock = LogFileLock(path)
90
+ acquired.append(lock)
91
+ locks[path] = lock
92
+ except Exception:
93
+ for lock in reversed(acquired):
94
+ lock.close()
95
+ raise
96
+ return locks
97
+
98
+
99
+ class ExclusiveRotatingFileHandler(RotatingFileHandler):
100
+ """Do not reopen a closed handler or write through a fork-inherited handler."""
101
+
102
+ def __init__(self, *args, max_age_days=DEFAULT_MAX_AGE_DAYS, **kwargs):
103
+ self.owner_pid = os.getpid()
104
+ self.max_age_days = max_age_days
105
+ super().__init__(*args, **kwargs)
106
+
107
+ def emit(self, record):
108
+ if not self._closed and self.owner_pid == os.getpid():
109
+ super().emit(record)
110
+
111
+ def doRollover(self):
112
+ """Keep numbered backups while recording their age from the rotation time."""
113
+ super().doRollover()
114
+ if self.backupCount > 0:
115
+ try:
116
+ os.utime(f"{self.baseFilename}.1", None)
117
+ except OSError as error:
118
+ _maintenance_logger.warning("Cannot update rotated log age: %s", error)
119
+ cutoff = time.time() - self.max_age_days * 86400 if self.max_age_days else None
120
+ _cleanup_backups(Path(self.baseFilename), cutoff, self.backupCount)
121
+
122
+
123
+ def _remove_file(path, cutoff=None):
124
+ try:
125
+ info = path.stat(follow_symlinks=False)
126
+ if stat.S_ISREG(info.st_mode) and (cutoff is None or info.st_mtime < cutoff):
127
+ path.unlink()
128
+ except FileNotFoundError:
129
+ pass
130
+ except OSError as error:
131
+ _maintenance_logger.warning("Cannot clean expired log file %s: %s", path, error)
132
+
133
+
134
+ def _cleanup_backups(base, cutoff, backup_count=0):
135
+ pattern = re.compile(re.escape(base.name) + r"\.([1-9][0-9]*)")
136
+ try:
137
+ for path in base.parent.iterdir():
138
+ match = pattern.fullmatch(path.name)
139
+ if match:
140
+ if backup_count > 0 and int(match[1]) > backup_count:
141
+ _remove_file(path)
142
+ elif cutoff is not None:
143
+ _remove_file(path, cutoff)
144
+ except OSError as error:
145
+ _maintenance_logger.warning("Cannot scan log backups for %s: %s", base, error)
146
+
147
+
148
+ class LogRetention:
149
+ """Clean owned backups and expired, inactive UUID-named context files."""
150
+
151
+ def __init__(self, locks, max_age_days, backup_count, family_paths=()):
152
+ self.locks = locks
153
+ self.max_age_days = max_age_days
154
+ self.backup_count = backup_count
155
+ self.family_paths = tuple(family_paths)
156
+ self.handlers = {}
157
+ self.lock = RLock()
158
+ self.stopping = Event()
159
+ self.thread = None
160
+ self.owner_pid = os.getpid()
161
+
162
+ def add_handler(self, handler):
163
+ """Serialize lazy handler creation with maintenance of its numbered backups."""
164
+ with self.lock:
165
+ self.handlers[Path(handler.baseFilename)] = handler
166
+
167
+ def _inactive_files(self):
168
+ candidates = set()
169
+ for family in self.family_paths:
170
+ pattern = re.compile("(" + re.escape(family.stem) + "-" + _UUID_PATTERN
171
+ + re.escape(family.suffix) + r")(?:\.[1-9][0-9]*)?")
172
+ try:
173
+ for path in family.parent.iterdir():
174
+ match = pattern.fullmatch(path.name)
175
+ if match:
176
+ candidates.add(path.with_name(match[1]))
177
+ except FileNotFoundError:
178
+ pass
179
+ except OSError as error:
180
+ _maintenance_logger.warning("Cannot scan context logs in %s: %s", family.parent, error)
181
+ return candidates - self.locks.keys()
182
+
183
+ def cleanup(self):
184
+ """Skip active owners and symlinks; retry failed deletions on the next scan."""
185
+ if self.owner_pid != os.getpid():
186
+ return
187
+ with self.lock:
188
+ cutoff = time.time() - self.max_age_days * 86400 if self.max_age_days else None
189
+ for path in self.locks:
190
+ handler = self.handlers.get(path)
191
+ if handler is not None:
192
+ handler.acquire()
193
+ try:
194
+ _cleanup_backups(path, cutoff, self.backup_count)
195
+ finally:
196
+ if handler is not None:
197
+ handler.release()
198
+ if cutoff is None:
199
+ return
200
+ for path in self._inactive_files():
201
+ if path.is_symlink():
202
+ continue
203
+ try:
204
+ owner = LogFileLock(path)
205
+ except SDKError:
206
+ continue
207
+ except OSError as error:
208
+ _maintenance_logger.warning("Cannot lock inactive log %s: %s", path, error)
209
+ continue
210
+ try:
211
+ _cleanup_backups(path, cutoff)
212
+ _remove_file(path, cutoff)
213
+ finally:
214
+ owner.close()
215
+
216
+ def start(self):
217
+ """Clean on startup, then hourly even if no log entries are emitted."""
218
+ self.cleanup()
219
+ if self.max_age_days:
220
+ thread = Thread(target=self._run, name="polaris-log-retention", daemon=True)
221
+ thread.start()
222
+ self.thread = thread
223
+
224
+ def _run(self):
225
+ while not self.stopping.wait(CLEANUP_INTERVAL_SECONDS):
226
+ self.cleanup()
227
+
228
+ def close(self):
229
+ """Finish maintenance before the caller closes handlers and releases locks."""
230
+ self.stopping.set()
231
+ if self.thread is not None and self.owner_pid == os.getpid():
232
+ self.thread.join()
polaris/api/config.py ADDED
@@ -0,0 +1,145 @@
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 import plugin_manager
17
+ from polaris.pkg.flow.impl import create_flow_engine, BaseEngine
18
+ from polaris.pkg.config.api import Configuration
19
+ from polaris.pkg.model.error import SDKError, ErrorCode
20
+ from polaris.pkg.model import context
21
+ from threading import RLock
22
+ import copy
23
+
24
+ from polaris.api.log import ContextLogger, LogOptions, use_context_logger
25
+
26
+
27
+ class SDKContext(object):
28
+ """Own the configuration, plugins and runtime for an independent SDK client."""
29
+ def __init__(self, config: Configuration, plugins: plugin_manager.PluginManager, engine: BaseEngine,
30
+ value_context: context.ValueContext = None, destroyed: int = 0):
31
+ self.config = config
32
+ self.plugins = plugins
33
+ self.engine = engine
34
+ self.value_context = value_context if value_context is not None else context.new_value_context()
35
+ self._destroy_lock = RLock()
36
+ # 标识是否已经销毁,0未销毁,1已销毁
37
+ self.destroyed = destroyed
38
+ self._cleaned_up = False
39
+ if context.ContextKeyEventCenter not in self.value_context.core_map:
40
+ from polaris.pkg.model.event import EventCenter
41
+ self.value_context.set_value(context.ContextKeyEventCenter, EventCenter(self.value_context))
42
+ add_callback = getattr(self.engine, "add_destroy_callback", None)
43
+ if add_callback is not None:
44
+ add_callback(self._cleanup)
45
+
46
+ def _cleanup(self):
47
+ if self._cleaned_up:
48
+ return
49
+ self._cleaned_up = True
50
+ try:
51
+ event_center = self.get_event_center()
52
+ if event_center is not None:
53
+ event_center.close()
54
+ self.plugins.destroy_plugins()
55
+ finally:
56
+ self.value_context.clear()
57
+ if self.value_context.logger is not None:
58
+ self.value_context.logger.close()
59
+
60
+ def destroy(self):
61
+ """Release only this context's resources; repeated destruction is harmless."""
62
+ with self._destroy_lock:
63
+ if self.is_destroy():
64
+ return
65
+ self.destroyed = 1
66
+ try:
67
+ self.engine.destroy()
68
+ finally:
69
+ if getattr(self.engine, "add_destroy_callback", None) is None:
70
+ self._cleanup()
71
+
72
+ def is_destroy(self):
73
+ """Report whether this client is no longer available."""
74
+ return self.destroyed > 0
75
+
76
+ def get_config(self):
77
+ """Return the configuration belonging to this client."""
78
+ return self.config
79
+
80
+ def get_plugins(self):
81
+ """Return this client's plugin manager."""
82
+ return self.plugins
83
+
84
+ def get_engine(self):
85
+ """Return the runtime owned by this client."""
86
+ return self.engine
87
+
88
+ def get_value_context(self):
89
+ """Return this client's identity and runtime values."""
90
+ return self.value_context
91
+
92
+ def get_client_id(self) -> str:
93
+ """Return the unique, stable client ID sent to Polaris."""
94
+ return self.value_context.get_client_id()
95
+
96
+ def get_event_center(self):
97
+ """Return this context's isolated event center."""
98
+ return self.value_context.core_map.get(context.ContextKeyEventCenter)
99
+
100
+
101
+ def init_context_by_file(path: str, apply_env: bool = True, *, log_options: LogOptions | None = None):
102
+ """Initialize an SDK context from a YAML file."""
103
+ return init_context_by_config(Configuration.from_file(path, apply_env=apply_env), log_options=log_options)
104
+
105
+
106
+ def init_context_by_stream(buf, apply_env: bool = True, *, log_options: LogOptions | None = None):
107
+ """Initialize an SDK context from YAML text or bytes."""
108
+ from polaris.pkg.config.loader import build_configuration, parse_config_stream
109
+ return init_context_by_config(
110
+ build_configuration(parse_config_stream(buf), apply_env=apply_env), log_options=log_options,
111
+ )
112
+
113
+
114
+ def init_context(*, log_options: LogOptions | None = None):
115
+ """Initialize an SDK context from the default file or environment."""
116
+ from polaris.pkg.config.loader import find_default_config_file
117
+
118
+ config_file = find_default_config_file()
119
+ if config_file is not None:
120
+ return init_context_by_file(str(config_file), log_options=log_options)
121
+ return init_context_by_config(Configuration.from_env(), log_options=log_options)
122
+
123
+
124
+ def init_context_by_config(config: Configuration, *, log_options: LogOptions | None = None):
125
+ """Initialize from assigned configuration values without loading files or environment overrides."""
126
+ config = copy.deepcopy(config)
127
+ config.set_default()
128
+ config.verify()
129
+ global_ctx = context.new_value_context()
130
+ global_ctx.logger = ContextLogger(global_ctx.get_client_id(), log_options)
131
+ from polaris.pkg.model.event import EventCenter
132
+ event_center = EventCenter(global_ctx)
133
+ global_ctx.set_value(context.ContextKeyEventCenter, event_center)
134
+ plugins = plugin_manager.DefaultPluginManager.new_context_manager()
135
+ try:
136
+ with use_context_logger(global_ctx.logger):
137
+ engine = create_flow_engine(config, plugins, global_ctx)
138
+ except Exception:
139
+ try:
140
+ plugins.destroy_plugins()
141
+ finally:
142
+ global_ctx.clear()
143
+ global_ctx.logger.close()
144
+ raise
145
+ return SDKContext(config=config, plugins=plugins, engine=engine, value_context=global_ctx)