surveycore-py 0.1.0__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 (121) hide show
  1. surveycore/__init__.py +44 -0
  2. surveycore/_question_metas.py +59 -0
  3. surveycore/answers/__init__.py +5 -0
  4. surveycore/answers/strategies.py +23 -0
  5. surveycore/app/__init__.py +1 -0
  6. surveycore/app/config.py +28 -0
  7. surveycore/app/runtime_paths.py +16 -0
  8. surveycore/app/user_paths.py +2 -0
  9. surveycore/client.py +187 -0
  10. surveycore/config.py +166 -0
  11. surveycore/core/__init__.py +0 -0
  12. surveycore/core/ai/__init__.py +3 -0
  13. surveycore/core/ai/batch_runtime.py +42 -0
  14. surveycore/core/ai/runtime.py +76 -0
  15. surveycore/core/config/__init__.py +0 -0
  16. surveycore/core/engine/__init__.py +0 -0
  17. surveycore/core/engine/async_events.py +90 -0
  18. surveycore/core/engine/async_scheduler.py +96 -0
  19. surveycore/core/engine/async_status_bus.py +50 -0
  20. surveycore/core/engine/async_wait.py +58 -0
  21. surveycore/core/engine/provider_common.py +150 -0
  22. surveycore/core/engine/runtime_actions.py +63 -0
  23. surveycore/core/engine/stop_signal.py +14 -0
  24. surveycore/core/modes/__init__.py +1 -0
  25. surveycore/core/modes/duration_control.py +206 -0
  26. surveycore/core/persona/__init__.py +0 -0
  27. surveycore/core/persona/context.py +139 -0
  28. surveycore/core/persona/generator.py +221 -0
  29. surveycore/core/psychometrics/__init__.py +62 -0
  30. surveycore/core/psychometrics/joint_optimizer.py +624 -0
  31. surveycore/core/psychometrics/ordinal_options.py +188 -0
  32. surveycore/core/psychometrics/orientation.py +190 -0
  33. surveycore/core/psychometrics/psychometric.py +334 -0
  34. surveycore/core/psychometrics/utils.py +159 -0
  35. surveycore/core/questions/__init__.py +0 -0
  36. surveycore/core/questions/config.py +33 -0
  37. surveycore/core/questions/consistency.py +337 -0
  38. surveycore/core/questions/default_builder.py +383 -0
  39. surveycore/core/questions/distribution.py +148 -0
  40. surveycore/core/questions/meta_helpers.py +165 -0
  41. surveycore/core/questions/normalization.py +393 -0
  42. surveycore/core/questions/reliability_mode.py +38 -0
  43. surveycore/core/questions/schema.py +104 -0
  44. surveycore/core/questions/strict_ratio.py +152 -0
  45. surveycore/core/questions/tendency.py +352 -0
  46. surveycore/core/questions/text_shared.py +3 -0
  47. surveycore/core/questions/text_values.py +136 -0
  48. surveycore/core/questions/types/__init__.py +0 -0
  49. surveycore/core/questions/utils.py +465 -0
  50. surveycore/core/questions/validation.py +213 -0
  51. surveycore/core/reverse_fill/__init__.py +49 -0
  52. surveycore/core/reverse_fill/parser.py +295 -0
  53. surveycore/core/reverse_fill/runtime.py +43 -0
  54. surveycore/core/reverse_fill/schema.py +151 -0
  55. surveycore/core/task/__init__.py +11 -0
  56. surveycore/core/task/distribution_state.py +235 -0
  57. surveycore/core/task/progress_state.py +246 -0
  58. surveycore/core/task/reverse_fill_state.py +189 -0
  59. surveycore/core/task/task_context.py +280 -0
  60. surveycore/core/user_agent.py +11 -0
  61. surveycore/credamo/__init__.py +0 -0
  62. surveycore/credamo/provider/__init__.py +0 -0
  63. surveycore/credamo/provider/answering_builders.py +185 -0
  64. surveycore/credamo/provider/http_runtime.py +902 -0
  65. surveycore/credamo/provider/parser.py +566 -0
  66. surveycore/data/area_codes_2022.json +1680 -0
  67. surveycore/errors.py +22 -0
  68. surveycore/io/__init__.py +0 -0
  69. surveycore/io/spreadsheets/__init__.py +0 -0
  70. surveycore/io/spreadsheets/wjx_excel.py +149 -0
  71. surveycore/logging/__init__.py +0 -0
  72. surveycore/logging/log_utils.py +43 -0
  73. surveycore/models.py +134 -0
  74. surveycore/network/__init__.py +1 -0
  75. surveycore/network/http/__init__.py +63 -0
  76. surveycore/network/session_policy.py +25 -0
  77. surveycore/providers/__init__.py +0 -0
  78. surveycore/providers/adapter_base.py +158 -0
  79. surveycore/providers/answering/__init__.py +7 -0
  80. surveycore/providers/answering/actions.py +49 -0
  81. surveycore/providers/answering/option_fill.py +71 -0
  82. surveycore/providers/answering/recording.py +55 -0
  83. surveycore/providers/answering/selection.py +108 -0
  84. surveycore/providers/common.py +174 -0
  85. surveycore/providers/contracts.py +435 -0
  86. surveycore/providers/errors.py +40 -0
  87. surveycore/providers/hooks.py +66 -0
  88. surveycore/providers/http_logic.py +285 -0
  89. surveycore/providers/http_progress.py +51 -0
  90. surveycore/providers/match_utils.py +54 -0
  91. surveycore/providers/registry.py +261 -0
  92. surveycore/providers/runtime_state.py +107 -0
  93. surveycore/py.typed +0 -0
  94. surveycore/runtime/__init__.py +5 -0
  95. surveycore/runtime/scheduler.py +30 -0
  96. surveycore/tencent/__init__.py +0 -0
  97. surveycore/tencent/provider/__init__.py +0 -0
  98. surveycore/tencent/provider/answering_builders.py +609 -0
  99. surveycore/tencent/provider/answering_rules.py +61 -0
  100. surveycore/tencent/provider/http_runtime.py +670 -0
  101. surveycore/tencent/provider/parser.py +1006 -0
  102. surveycore/transport.py +12 -0
  103. surveycore/wjx/__init__.py +1 -0
  104. surveycore/wjx/provider/__init__.py +0 -0
  105. surveycore/wjx/provider/answering_builders.py +727 -0
  106. surveycore/wjx/provider/html_parser.py +356 -0
  107. surveycore/wjx/provider/html_parser_choice.py +481 -0
  108. surveycore/wjx/provider/html_parser_common.py +586 -0
  109. surveycore/wjx/provider/html_parser_matrix.py +370 -0
  110. surveycore/wjx/provider/html_parser_rules.py +394 -0
  111. surveycore/wjx/provider/http_runtime.py +586 -0
  112. surveycore/wjx/provider/parser.py +227 -0
  113. surveycore/wjx/provider/questions/__init__.py +0 -0
  114. surveycore/wjx/provider/questions/_multiple_limit_patterns.py +130 -0
  115. surveycore/wjx/provider/questions/multiple_limits.py +237 -0
  116. surveycore/wjx/provider/questions/multiple_rules.py +115 -0
  117. surveycore/wjx/provider/regexes.py +164 -0
  118. surveycore_py-0.1.0.dist-info/METADATA +145 -0
  119. surveycore_py-0.1.0.dist-info/RECORD +121 -0
  120. surveycore_py-0.1.0.dist-info/WHEEL +4 -0
  121. surveycore_py-0.1.0.dist-info/licenses/LICENSE +674 -0
File without changes
File without changes
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable, Optional
6
+
7
+ from surveycore.core.task import ExecutionState
8
+
9
+
10
+ class AsyncRunContext:
11
+
12
+
13
+ def __init__(
14
+ self,
15
+ *,
16
+ state: ExecutionState,
17
+ stop_event: asyncio.Event,
18
+ pause_event: asyncio.Event,
19
+ status_sink: Optional[Callable[[dict[str, Any]], None]] = None,
20
+ ) -> None:
21
+ self.state = state
22
+ self.stop_event = stop_event
23
+ self.pause_event = pause_event
24
+ self.status_sink = status_sink
25
+
26
+ def stop_requested(self) -> bool:
27
+ return bool(self.stop_event.is_set())
28
+
29
+ async def wait_if_paused(self) -> None:
30
+ while self.pause_event.is_set() and not self.stop_event.is_set():
31
+ await asyncio.sleep(0.1)
32
+
33
+ def emit(self, event: dict[str, Any]) -> None:
34
+ sink = self.status_sink
35
+ if callable(sink):
36
+ sink(dict(event or {}))
37
+
38
+
39
+ class ThreadEventProxy:
40
+
41
+
42
+ def __init__(self, event: asyncio.Event, *, loop: asyncio.AbstractEventLoop) -> None:
43
+ self._event = event
44
+ self._loop = loop
45
+
46
+ def is_set(self) -> bool:
47
+ return bool(self._event.is_set())
48
+
49
+ def set(self) -> None:
50
+ if self._loop.is_closed():
51
+ return
52
+ self._loop.call_soon_threadsafe(self._event.set)
53
+
54
+ def clear(self) -> None:
55
+ if self._loop.is_closed():
56
+ return
57
+ self._loop.call_soon_threadsafe(self._event.clear)
58
+
59
+ def wait(self, timeout: Optional[float] = None) -> bool:
60
+ if self.is_set():
61
+ return True
62
+ try:
63
+ running_loop = asyncio.get_running_loop()
64
+ except RuntimeError:
65
+ running_loop = None
66
+ if running_loop is self._loop:
67
+ raise RuntimeError("ThreadEventProxy.wait() 不能在绑定的事件循环线程里阻塞调用")
68
+
69
+ async def _wait() -> bool:
70
+ try:
71
+ if timeout is None:
72
+ await self._event.wait()
73
+ return True
74
+ await asyncio.wait_for(self._event.wait(), timeout=max(0.0, float(timeout)))
75
+ return True
76
+ except asyncio.TimeoutError:
77
+ return bool(self._event.is_set())
78
+
79
+ future = asyncio.run_coroutine_threadsafe(_wait(), self._loop)
80
+ return bool(future.result())
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class AsyncSlotResult:
85
+ slot_id: int
86
+ completed: bool
87
+ error: Optional[BaseException] = None
88
+
89
+
90
+ __all__ = ["AsyncRunContext", "AsyncSlotResult", "ThreadEventProxy"]
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import heapq
5
+ import itertools
6
+ import time
7
+ from collections import deque
8
+ from dataclasses import dataclass, field
9
+ from typing import Optional
10
+
11
+
12
+ @dataclass(order=True)
13
+ class _ScheduledToken:
14
+ ready_at: float
15
+ order: int
16
+ token_id: int = field(compare=False)
17
+
18
+
19
+ class AsyncScheduler:
20
+
21
+
22
+ def __init__(self, *, concurrency: int) -> None:
23
+ self._concurrency = max(1, int(concurrency or 1))
24
+ self._ready: deque[int] = deque()
25
+ self._delayed: list[_ScheduledToken] = []
26
+ self._order = itertools.count()
27
+ self._condition = asyncio.Condition()
28
+ self._closed = False
29
+ self._waker_task: Optional[asyncio.Task[None]] = None
30
+ for token_id in range(self._concurrency):
31
+ self._ready.append(token_id)
32
+
33
+ async def start(self) -> None:
34
+ if self._waker_task is None:
35
+ self._waker_task = asyncio.create_task(self._wake_delayed_tokens(), name="AsyncSchedulerWake")
36
+
37
+ async def acquire(self) -> Optional[int]:
38
+ await self.start()
39
+ async with self._condition:
40
+ while True:
41
+ if self._closed:
42
+ return None
43
+ if self._ready:
44
+ return self._ready.popleft()
45
+ await self._condition.wait()
46
+
47
+ async def release(self, token_id: int, *, requeue: bool, delay_seconds: float = 0.0) -> None:
48
+ if not requeue:
49
+ return
50
+ delay = max(0.0, float(delay_seconds or 0.0))
51
+ async with self._condition:
52
+ if self._closed:
53
+ return
54
+ if delay <= 0:
55
+ self._ready.append(int(token_id))
56
+ self._condition.notify_all()
57
+ return
58
+ heapq.heappush(
59
+ self._delayed,
60
+ _ScheduledToken(time.monotonic() + delay, next(self._order), int(token_id)),
61
+ )
62
+ self._condition.notify_all()
63
+
64
+ async def _wake_delayed_tokens(self) -> None:
65
+ while True:
66
+ async with self._condition:
67
+ if self._closed:
68
+ return
69
+ now = time.monotonic()
70
+ moved = False
71
+ while self._delayed and self._delayed[0].ready_at <= now:
72
+ token = heapq.heappop(self._delayed)
73
+ self._ready.append(token.token_id)
74
+ moved = True
75
+ if moved:
76
+ self._condition.notify_all()
77
+ continue
78
+ sleep_for = 0.1
79
+ if self._delayed:
80
+ sleep_for = min(0.1, max(0.0, self._delayed[0].ready_at - now))
81
+ try:
82
+ await asyncio.sleep(sleep_for)
83
+ except asyncio.CancelledError:
84
+ break
85
+
86
+ async def close(self) -> None:
87
+ self._closed = True
88
+ if self._waker_task is not None:
89
+ self._waker_task.cancel()
90
+ await asyncio.gather(self._waker_task, return_exceptions=True)
91
+ self._waker_task = None
92
+ async with self._condition:
93
+ self._condition.notify_all()
94
+
95
+
96
+ __all__ = ["AsyncScheduler"]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import time
5
+ from collections import defaultdict
6
+ from typing import Any, Callable, DefaultDict, Optional
7
+
8
+
9
+ class AsyncStatusBus:
10
+
11
+
12
+ def __init__(
13
+ self,
14
+ *,
15
+ dispatcher: Optional[Callable[[Callable[[], Any]], Any]] = None,
16
+ throttle_seconds: float = 0.075,
17
+ ) -> None:
18
+ self._dispatcher = dispatcher
19
+ self._throttle_seconds = max(0.0, float(throttle_seconds or 0.0))
20
+ self._lock = threading.Lock()
21
+ self._sequence_by_slot: DefaultDict[str, int] = defaultdict(int)
22
+ self._last_emit_by_slot: dict[str, float] = {}
23
+
24
+ def emit(self, event: dict[str, Any]) -> None:
25
+ payload = dict(event or {})
26
+ slot_id = str(payload.get("slot_id") or payload.get("slot_label") or "global")
27
+ now = time.monotonic()
28
+ with self._lock:
29
+ self._sequence_by_slot[slot_id] += 1
30
+ payload["sequence"] = self._sequence_by_slot[slot_id]
31
+ event_type = str(payload.get("type") or "")
32
+ high_frequency = event_type in {"progress", "status"}
33
+ last_emit = self._last_emit_by_slot.get(slot_id, 0.0)
34
+ if high_frequency and self._throttle_seconds > 0 and (now - last_emit) < self._throttle_seconds:
35
+ return
36
+ self._last_emit_by_slot[slot_id] = now
37
+
38
+ dispatcher = self._dispatcher
39
+ if not callable(dispatcher):
40
+ return
41
+
42
+ def _deliver() -> None:
43
+ callback = payload.get("callback")
44
+ if callable(callback):
45
+ callback()
46
+
47
+ dispatcher(_deliver)
48
+
49
+
50
+ __all__ = ["AsyncStatusBus"]
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, Optional
5
+
6
+
7
+ def is_stop_requested(stop_signal: Any) -> bool:
8
+ if stop_signal is None:
9
+ return False
10
+ checker = getattr(stop_signal, "is_set", None)
11
+ if callable(checker):
12
+ try:
13
+ return bool(checker())
14
+ except Exception:
15
+ return False
16
+ return False
17
+
18
+
19
+ def _resolve_async_event(stop_signal: Any) -> Optional[asyncio.Event]:
20
+ if isinstance(stop_signal, asyncio.Event):
21
+ return stop_signal
22
+ event = getattr(stop_signal, "_event", None)
23
+ if isinstance(event, asyncio.Event):
24
+ return event
25
+ return None
26
+
27
+
28
+ async def sleep_or_stop(stop_signal: Any, seconds: float) -> bool:
29
+ delay = max(0.0, float(seconds or 0.0))
30
+ if delay <= 0:
31
+ return is_stop_requested(stop_signal)
32
+
33
+ async_event = _resolve_async_event(stop_signal)
34
+ if async_event is not None:
35
+ if async_event.is_set():
36
+ return True
37
+ try:
38
+ await asyncio.wait_for(async_event.wait(), timeout=delay)
39
+ return True
40
+ except asyncio.TimeoutError:
41
+ return bool(async_event.is_set())
42
+
43
+ if stop_signal is None:
44
+ await asyncio.sleep(delay)
45
+ return False
46
+
47
+ waiter = getattr(stop_signal, "wait", None)
48
+ if callable(waiter):
49
+ try:
50
+ return bool(await asyncio.to_thread(waiter, delay))
51
+ except Exception:
52
+ pass
53
+
54
+ await asyncio.sleep(delay)
55
+ return is_stop_requested(stop_signal)
56
+
57
+
58
+ __all__ = ["is_stop_requested", "sleep_or_stop"]
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import contextmanager
5
+ from typing import Any, Dict, Iterator, List, Optional
6
+
7
+ from surveycore.core.persona.context import reset_context as _reset_answer_context
8
+ from surveycore.core.persona.generator import generate_persona, reset_persona, set_current_persona
9
+ from surveycore.core.psychometrics import (
10
+ CombinedPsychometricPlan,
11
+ build_dimension_psychometric_plan,
12
+ build_joint_psychometric_answer_plan,
13
+ build_psychometric_blueprint,
14
+ )
15
+ from surveycore.core.psychometrics.psychometric import normalize_target_alpha
16
+ from surveycore.core.questions.config import GLOBAL_RELIABILITY_DIMENSION
17
+ from surveycore.core.questions.consistency import reset_consistency_context
18
+ from surveycore.core.questions.tendency import reset_tendency
19
+ from surveycore.core.task import ExecutionConfig, ExecutionState
20
+
21
+
22
+ def _build_grouped_runtime_items(
23
+ config: ExecutionConfig,
24
+ ) -> Dict[str, List[Any]]:
25
+ grouped_items: Dict[str, List[Any]] = {}
26
+ for dimension, items in build_psychometric_blueprint(config).items():
27
+ normalized_dimension = str(dimension or "").strip()
28
+ if not normalized_dimension:
29
+ continue
30
+ bucket = grouped_items.setdefault(normalized_dimension, [])
31
+ for item in items:
32
+ bucket.append(item.to_runtime_item())
33
+ return grouped_items
34
+
35
+
36
+ def build_psychometric_plan_for_run(config: ExecutionConfig) -> Optional[Any]:
37
+
38
+ grouped_items = _build_grouped_runtime_items(config)
39
+
40
+ if not grouped_items:
41
+ return None
42
+
43
+ try:
44
+ target_alpha = normalize_target_alpha(getattr(config, "psycho_target_alpha", None))
45
+ except Exception:
46
+ target_alpha = normalize_target_alpha(None)
47
+
48
+ return build_dimension_psychometric_plan(
49
+ grouped_items=grouped_items,
50
+ target_alpha=target_alpha,
51
+ )
52
+
53
+
54
+ def ensure_joint_psychometric_answer_plan(config: ExecutionConfig) -> Optional[Any]:
55
+ cached = getattr(config, "joint_psychometric_answer_plan", None)
56
+ if cached is not None:
57
+ return cached
58
+ plan = build_joint_psychometric_answer_plan(config)
59
+ config.joint_psychometric_answer_plan = plan
60
+ return plan
61
+
62
+
63
+ @contextmanager
64
+ def provider_run_context(
65
+ config: ExecutionConfig,
66
+ *,
67
+ state: Optional[ExecutionState] = None,
68
+ thread_name: str = "",
69
+ psycho_plan: Optional[Any] = None,
70
+ ) -> Iterator[Optional[Any]]:
71
+
72
+ persona = generate_persona()
73
+ set_current_persona(persona)
74
+ _reset_answer_context()
75
+ reset_tendency()
76
+ reset_consistency_context(config.answer_rules, list((config.questions_metadata or {}).values()))
77
+
78
+ resolved_plan = psycho_plan
79
+ fallback_plan: Optional[Any] = None
80
+ joint_sample_plan: Optional[Any] = None
81
+ reserved_sample_index: Optional[int] = None
82
+ if resolved_plan is None:
83
+ fallback_plan = build_psychometric_plan_for_run(config)
84
+ joint_answer_plan = ensure_joint_psychometric_answer_plan(config)
85
+ if joint_answer_plan is not None and state is not None:
86
+ reserved_sample_index = state.peek_reserved_joint_sample(thread_name)
87
+ if reserved_sample_index is not None:
88
+ joint_sample_plan = joint_answer_plan.build_sample_plan(reserved_sample_index)
89
+ else:
90
+ logging.warning("线程[%s]存在联合信效度计划但未预留样本槽位,已回退常规逻辑", thread_name or "Worker-?")
91
+ if joint_sample_plan is not None and fallback_plan is not None:
92
+ resolved_plan = CombinedPsychometricPlan(primary=joint_sample_plan, fallback=fallback_plan)
93
+ elif joint_sample_plan is not None:
94
+ resolved_plan = joint_sample_plan
95
+ else:
96
+ resolved_plan = fallback_plan
97
+
98
+ if joint_sample_plan is not None:
99
+ diagnostics = dict(getattr(joint_sample_plan, "diagnostics_by_dimension", {}) or {})
100
+ active_dimensions = [
101
+ name
102
+ for name, diagnostic in diagnostics.items()
103
+ if not bool(getattr(diagnostic, "skipped", False))
104
+ ]
105
+ logging.debug(
106
+ "本轮启用联合信效度计划:样本槽位=%d,维度数=%d,锁定题目数=%d,目标α=%.2f,维度=%s",
107
+ int(reserved_sample_index or 0) + 1,
108
+ len(active_dimensions),
109
+ len(getattr(joint_sample_plan, "choices", {}) or {}),
110
+ float(getattr(config, "psycho_target_alpha", 0.85) or 0.85),
111
+ ",".join(active_dimensions[:5]) if active_dimensions else "无",
112
+ )
113
+ for diagnostic in diagnostics.values():
114
+ if bool(getattr(diagnostic, "skipped", False)):
115
+ continue
116
+ if not bool(getattr(diagnostic, "degraded_for_ratio", False)):
117
+ continue
118
+ logging.warning(
119
+ "维度[%s]已保比例优先,实际α=%.3f 低于目标α=%.3f,主方向=%s,反向题=%d,锚点明确=%s",
120
+ getattr(diagnostic, "dimension", ""),
121
+ float(getattr(diagnostic, "actual_alpha", 0.0) or 0.0),
122
+ float(getattr(diagnostic, "target_alpha", 0.0) or 0.0),
123
+ str(getattr(diagnostic, "anchor_direction", "center") or "center"),
124
+ int(getattr(diagnostic, "reverse_item_count", 0) or 0),
125
+ "否" if bool(getattr(diagnostic, "ambiguous_anchor", False)) else "是",
126
+ )
127
+ elif resolved_plan is not None:
128
+ dimension_count = len(getattr(resolved_plan, "plans", {}) or {})
129
+ plan_names = list((getattr(resolved_plan, "plans", {}) or {}).keys())
130
+ if plan_names == [GLOBAL_RELIABILITY_DIMENSION]:
131
+ dimension_summary = "全局未分组问卷"
132
+ else:
133
+ dimension_summary = ",".join(plan_names[:5]) if plan_names else "无"
134
+ logging.info(
135
+ "本轮启用心理测量计划:维度数=%d,题目数=%d,目标α=%.2f,维度=%s",
136
+ dimension_count,
137
+ len(getattr(resolved_plan, "items", []) or []),
138
+ float(getattr(config, "psycho_target_alpha", 0.85) or 0.85),
139
+ dimension_summary,
140
+ )
141
+
142
+ try:
143
+ yield resolved_plan
144
+ finally:
145
+ reset_persona()
146
+ __all__ = [
147
+ "build_psychometric_plan_for_run",
148
+ "ensure_joint_psychometric_answer_plan",
149
+ "provider_run_context",
150
+ ]
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Iterable, Optional, Tuple
6
+
7
+
8
+ class RuntimeActionKind(str, Enum):
9
+ PAUSE_RUN = "pause_run"
10
+ SHOW_MESSAGE = "show_message"
11
+ CONFIRM_ENABLE_RANDOM_IP = "confirm_enable_random_ip"
12
+ SET_RANDOM_IP_ENABLED = "set_random_ip_enabled"
13
+ REFRESH_RANDOM_IP_COUNTER = "refresh_random_ip_counter"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class RuntimeActionRequest:
18
+ kind: RuntimeActionKind
19
+ title: str = ""
20
+ message: str = ""
21
+ level: str = "info"
22
+ reason: str = ""
23
+ enabled: Optional[bool] = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class RuntimeActionResult:
28
+ actions: Tuple[RuntimeActionRequest, ...] = ()
29
+ should_stop: bool = False
30
+
31
+ @classmethod
32
+ def empty(cls) -> "RuntimeActionResult":
33
+ return cls()
34
+
35
+ @classmethod
36
+ def from_actions(
37
+ cls,
38
+ actions: Iterable[RuntimeActionRequest],
39
+ *,
40
+ should_stop: bool = False,
41
+ ) -> "RuntimeActionResult":
42
+ return cls(tuple(actions or ()), bool(should_stop))
43
+
44
+
45
+ def ensure_runtime_action_result(value: object) -> RuntimeActionResult:
46
+ if isinstance(value, RuntimeActionResult):
47
+ return value
48
+ if value is None:
49
+ return RuntimeActionResult.empty()
50
+ if isinstance(value, RuntimeActionRequest):
51
+ return RuntimeActionResult((value,))
52
+ if isinstance(value, (list, tuple)):
53
+ actions = tuple(item for item in value if isinstance(item, RuntimeActionRequest))
54
+ return RuntimeActionResult(actions)
55
+ return RuntimeActionResult.empty()
56
+
57
+
58
+ __all__ = [
59
+ "RuntimeActionKind",
60
+ "RuntimeActionRequest",
61
+ "RuntimeActionResult",
62
+ "ensure_runtime_action_result",
63
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Protocol
4
+
5
+
6
+ class StopSignalLike(Protocol):
7
+ def is_set(self) -> bool: ...
8
+
9
+ def set(self) -> None: ...
10
+
11
+ def wait(self, timeout: Optional[float] = None) -> bool: ...
12
+
13
+
14
+ __all__ = ["StopSignalLike"]
@@ -0,0 +1 @@
1
+ __all__: list[str] = []