chalkpy 2.89.22__py3-none-any.whl → 2.95.3__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 (268) hide show
  1. chalk/__init__.py +2 -1
  2. chalk/_gen/chalk/arrow/v1/arrow_pb2.py +7 -5
  3. chalk/_gen/chalk/arrow/v1/arrow_pb2.pyi +6 -0
  4. chalk/_gen/chalk/artifacts/v1/chart_pb2.py +36 -33
  5. chalk/_gen/chalk/artifacts/v1/chart_pb2.pyi +41 -1
  6. chalk/_gen/chalk/artifacts/v1/cron_query_pb2.py +8 -7
  7. chalk/_gen/chalk/artifacts/v1/cron_query_pb2.pyi +5 -0
  8. chalk/_gen/chalk/common/v1/offline_query_pb2.py +19 -13
  9. chalk/_gen/chalk/common/v1/offline_query_pb2.pyi +37 -0
  10. chalk/_gen/chalk/common/v1/online_query_pb2.py +54 -54
  11. chalk/_gen/chalk/common/v1/online_query_pb2.pyi +13 -1
  12. chalk/_gen/chalk/common/v1/script_task_pb2.py +13 -11
  13. chalk/_gen/chalk/common/v1/script_task_pb2.pyi +19 -1
  14. chalk/_gen/chalk/dataframe/__init__.py +0 -0
  15. chalk/_gen/chalk/dataframe/v1/__init__.py +0 -0
  16. chalk/_gen/chalk/dataframe/v1/dataframe_pb2.py +48 -0
  17. chalk/_gen/chalk/dataframe/v1/dataframe_pb2.pyi +123 -0
  18. chalk/_gen/chalk/dataframe/v1/dataframe_pb2_grpc.py +4 -0
  19. chalk/_gen/chalk/dataframe/v1/dataframe_pb2_grpc.pyi +4 -0
  20. chalk/_gen/chalk/graph/v1/graph_pb2.py +150 -149
  21. chalk/_gen/chalk/graph/v1/graph_pb2.pyi +25 -0
  22. chalk/_gen/chalk/graph/v1/sources_pb2.py +94 -84
  23. chalk/_gen/chalk/graph/v1/sources_pb2.pyi +56 -0
  24. chalk/_gen/chalk/kubernetes/v1/horizontalpodautoscaler_pb2.py +79 -0
  25. chalk/_gen/chalk/kubernetes/v1/horizontalpodautoscaler_pb2.pyi +377 -0
  26. chalk/_gen/chalk/kubernetes/v1/horizontalpodautoscaler_pb2_grpc.py +4 -0
  27. chalk/_gen/chalk/kubernetes/v1/horizontalpodautoscaler_pb2_grpc.pyi +4 -0
  28. chalk/_gen/chalk/kubernetes/v1/scaledobject_pb2.py +43 -7
  29. chalk/_gen/chalk/kubernetes/v1/scaledobject_pb2.pyi +252 -2
  30. chalk/_gen/chalk/protosql/v1/sql_service_pb2.py +54 -27
  31. chalk/_gen/chalk/protosql/v1/sql_service_pb2.pyi +131 -3
  32. chalk/_gen/chalk/protosql/v1/sql_service_pb2_grpc.py +45 -0
  33. chalk/_gen/chalk/protosql/v1/sql_service_pb2_grpc.pyi +14 -0
  34. chalk/_gen/chalk/python/v1/types_pb2.py +14 -14
  35. chalk/_gen/chalk/python/v1/types_pb2.pyi +8 -0
  36. chalk/_gen/chalk/server/v1/benchmark_pb2.py +76 -0
  37. chalk/_gen/chalk/server/v1/benchmark_pb2.pyi +156 -0
  38. chalk/_gen/chalk/server/v1/benchmark_pb2_grpc.py +258 -0
  39. chalk/_gen/chalk/server/v1/benchmark_pb2_grpc.pyi +84 -0
  40. chalk/_gen/chalk/server/v1/billing_pb2.py +40 -38
  41. chalk/_gen/chalk/server/v1/billing_pb2.pyi +17 -1
  42. chalk/_gen/chalk/server/v1/branches_pb2.py +45 -0
  43. chalk/_gen/chalk/server/v1/branches_pb2.pyi +80 -0
  44. chalk/_gen/chalk/server/v1/branches_pb2_grpc.pyi +36 -0
  45. chalk/_gen/chalk/server/v1/builder_pb2.py +372 -272
  46. chalk/_gen/chalk/server/v1/builder_pb2.pyi +479 -12
  47. chalk/_gen/chalk/server/v1/builder_pb2_grpc.py +360 -0
  48. chalk/_gen/chalk/server/v1/builder_pb2_grpc.pyi +96 -0
  49. chalk/_gen/chalk/server/v1/chart_pb2.py +10 -10
  50. chalk/_gen/chalk/server/v1/chart_pb2.pyi +18 -2
  51. chalk/_gen/chalk/server/v1/clickhouse_pb2.py +42 -0
  52. chalk/_gen/chalk/server/v1/clickhouse_pb2.pyi +17 -0
  53. chalk/_gen/chalk/server/v1/clickhouse_pb2_grpc.py +78 -0
  54. chalk/_gen/chalk/server/v1/clickhouse_pb2_grpc.pyi +38 -0
  55. chalk/_gen/chalk/server/v1/cloud_components_pb2.py +153 -107
  56. chalk/_gen/chalk/server/v1/cloud_components_pb2.pyi +146 -4
  57. chalk/_gen/chalk/server/v1/cloud_components_pb2_grpc.py +180 -0
  58. chalk/_gen/chalk/server/v1/cloud_components_pb2_grpc.pyi +48 -0
  59. chalk/_gen/chalk/server/v1/cloud_credentials_pb2.py +11 -3
  60. chalk/_gen/chalk/server/v1/cloud_credentials_pb2.pyi +20 -0
  61. chalk/_gen/chalk/server/v1/cloud_credentials_pb2_grpc.py +45 -0
  62. chalk/_gen/chalk/server/v1/cloud_credentials_pb2_grpc.pyi +12 -0
  63. chalk/_gen/chalk/server/v1/dataplanejobqueue_pb2.py +59 -35
  64. chalk/_gen/chalk/server/v1/dataplanejobqueue_pb2.pyi +127 -1
  65. chalk/_gen/chalk/server/v1/dataplanejobqueue_pb2_grpc.py +135 -0
  66. chalk/_gen/chalk/server/v1/dataplanejobqueue_pb2_grpc.pyi +36 -0
  67. chalk/_gen/chalk/server/v1/dataplaneworkflows_pb2.py +90 -0
  68. chalk/_gen/chalk/server/v1/dataplaneworkflows_pb2.pyi +264 -0
  69. chalk/_gen/chalk/server/v1/dataplaneworkflows_pb2_grpc.py +170 -0
  70. chalk/_gen/chalk/server/v1/dataplaneworkflows_pb2_grpc.pyi +62 -0
  71. chalk/_gen/chalk/server/v1/datasets_pb2.py +36 -24
  72. chalk/_gen/chalk/server/v1/datasets_pb2.pyi +71 -2
  73. chalk/_gen/chalk/server/v1/datasets_pb2_grpc.py +45 -0
  74. chalk/_gen/chalk/server/v1/datasets_pb2_grpc.pyi +12 -0
  75. chalk/_gen/chalk/server/v1/deploy_pb2.py +9 -3
  76. chalk/_gen/chalk/server/v1/deploy_pb2.pyi +12 -0
  77. chalk/_gen/chalk/server/v1/deploy_pb2_grpc.py +45 -0
  78. chalk/_gen/chalk/server/v1/deploy_pb2_grpc.pyi +12 -0
  79. chalk/_gen/chalk/server/v1/deployment_pb2.py +20 -15
  80. chalk/_gen/chalk/server/v1/deployment_pb2.pyi +25 -0
  81. chalk/_gen/chalk/server/v1/environment_pb2.py +25 -15
  82. chalk/_gen/chalk/server/v1/environment_pb2.pyi +93 -1
  83. chalk/_gen/chalk/server/v1/eventbus_pb2.py +44 -0
  84. chalk/_gen/chalk/server/v1/eventbus_pb2.pyi +64 -0
  85. chalk/_gen/chalk/server/v1/eventbus_pb2_grpc.py +4 -0
  86. chalk/_gen/chalk/server/v1/eventbus_pb2_grpc.pyi +4 -0
  87. chalk/_gen/chalk/server/v1/files_pb2.py +65 -0
  88. chalk/_gen/chalk/server/v1/files_pb2.pyi +167 -0
  89. chalk/_gen/chalk/server/v1/files_pb2_grpc.py +4 -0
  90. chalk/_gen/chalk/server/v1/files_pb2_grpc.pyi +4 -0
  91. chalk/_gen/chalk/server/v1/graph_pb2.py +41 -3
  92. chalk/_gen/chalk/server/v1/graph_pb2.pyi +191 -0
  93. chalk/_gen/chalk/server/v1/graph_pb2_grpc.py +92 -0
  94. chalk/_gen/chalk/server/v1/graph_pb2_grpc.pyi +32 -0
  95. chalk/_gen/chalk/server/v1/incident_pb2.py +57 -0
  96. chalk/_gen/chalk/server/v1/incident_pb2.pyi +165 -0
  97. chalk/_gen/chalk/server/v1/incident_pb2_grpc.py +4 -0
  98. chalk/_gen/chalk/server/v1/incident_pb2_grpc.pyi +4 -0
  99. chalk/_gen/chalk/server/v1/indexing_job_pb2.py +44 -0
  100. chalk/_gen/chalk/server/v1/indexing_job_pb2.pyi +38 -0
  101. chalk/_gen/chalk/server/v1/indexing_job_pb2_grpc.py +78 -0
  102. chalk/_gen/chalk/server/v1/indexing_job_pb2_grpc.pyi +38 -0
  103. chalk/_gen/chalk/server/v1/integrations_pb2.py +11 -9
  104. chalk/_gen/chalk/server/v1/integrations_pb2.pyi +34 -2
  105. chalk/_gen/chalk/server/v1/kube_pb2.py +29 -19
  106. chalk/_gen/chalk/server/v1/kube_pb2.pyi +28 -0
  107. chalk/_gen/chalk/server/v1/kube_pb2_grpc.py +45 -0
  108. chalk/_gen/chalk/server/v1/kube_pb2_grpc.pyi +12 -0
  109. chalk/_gen/chalk/server/v1/log_pb2.py +21 -3
  110. chalk/_gen/chalk/server/v1/log_pb2.pyi +68 -0
  111. chalk/_gen/chalk/server/v1/log_pb2_grpc.py +90 -0
  112. chalk/_gen/chalk/server/v1/log_pb2_grpc.pyi +24 -0
  113. chalk/_gen/chalk/server/v1/metadataplanejobqueue_pb2.py +73 -0
  114. chalk/_gen/chalk/server/v1/metadataplanejobqueue_pb2.pyi +212 -0
  115. chalk/_gen/chalk/server/v1/metadataplanejobqueue_pb2_grpc.py +217 -0
  116. chalk/_gen/chalk/server/v1/metadataplanejobqueue_pb2_grpc.pyi +74 -0
  117. chalk/_gen/chalk/server/v1/model_registry_pb2.py +10 -10
  118. chalk/_gen/chalk/server/v1/model_registry_pb2.pyi +4 -1
  119. chalk/_gen/chalk/server/v1/monitoring_pb2.py +84 -75
  120. chalk/_gen/chalk/server/v1/monitoring_pb2.pyi +1 -0
  121. chalk/_gen/chalk/server/v1/monitoring_pb2_grpc.py +136 -0
  122. chalk/_gen/chalk/server/v1/monitoring_pb2_grpc.pyi +38 -0
  123. chalk/_gen/chalk/server/v1/offline_queries_pb2.py +32 -10
  124. chalk/_gen/chalk/server/v1/offline_queries_pb2.pyi +73 -0
  125. chalk/_gen/chalk/server/v1/offline_queries_pb2_grpc.py +90 -0
  126. chalk/_gen/chalk/server/v1/offline_queries_pb2_grpc.pyi +24 -0
  127. chalk/_gen/chalk/server/v1/plandebug_pb2.py +53 -0
  128. chalk/_gen/chalk/server/v1/plandebug_pb2.pyi +86 -0
  129. chalk/_gen/chalk/server/v1/plandebug_pb2_grpc.py +168 -0
  130. chalk/_gen/chalk/server/v1/plandebug_pb2_grpc.pyi +60 -0
  131. chalk/_gen/chalk/server/v1/queries_pb2.py +76 -48
  132. chalk/_gen/chalk/server/v1/queries_pb2.pyi +155 -2
  133. chalk/_gen/chalk/server/v1/queries_pb2_grpc.py +180 -0
  134. chalk/_gen/chalk/server/v1/queries_pb2_grpc.pyi +48 -0
  135. chalk/_gen/chalk/server/v1/scheduled_query_pb2.py +4 -2
  136. chalk/_gen/chalk/server/v1/scheduled_query_pb2_grpc.py +45 -0
  137. chalk/_gen/chalk/server/v1/scheduled_query_pb2_grpc.pyi +12 -0
  138. chalk/_gen/chalk/server/v1/scheduled_query_run_pb2.py +12 -6
  139. chalk/_gen/chalk/server/v1/scheduled_query_run_pb2.pyi +75 -2
  140. chalk/_gen/chalk/server/v1/scheduler_pb2.py +24 -12
  141. chalk/_gen/chalk/server/v1/scheduler_pb2.pyi +61 -1
  142. chalk/_gen/chalk/server/v1/scheduler_pb2_grpc.py +90 -0
  143. chalk/_gen/chalk/server/v1/scheduler_pb2_grpc.pyi +24 -0
  144. chalk/_gen/chalk/server/v1/script_tasks_pb2.py +26 -14
  145. chalk/_gen/chalk/server/v1/script_tasks_pb2.pyi +33 -3
  146. chalk/_gen/chalk/server/v1/script_tasks_pb2_grpc.py +90 -0
  147. chalk/_gen/chalk/server/v1/script_tasks_pb2_grpc.pyi +24 -0
  148. chalk/_gen/chalk/server/v1/sql_interface_pb2.py +75 -0
  149. chalk/_gen/chalk/server/v1/sql_interface_pb2.pyi +142 -0
  150. chalk/_gen/chalk/server/v1/sql_interface_pb2_grpc.py +349 -0
  151. chalk/_gen/chalk/server/v1/sql_interface_pb2_grpc.pyi +114 -0
  152. chalk/_gen/chalk/server/v1/sql_queries_pb2.py +48 -0
  153. chalk/_gen/chalk/server/v1/sql_queries_pb2.pyi +150 -0
  154. chalk/_gen/chalk/server/v1/sql_queries_pb2_grpc.py +123 -0
  155. chalk/_gen/chalk/server/v1/sql_queries_pb2_grpc.pyi +52 -0
  156. chalk/_gen/chalk/server/v1/team_pb2.py +156 -137
  157. chalk/_gen/chalk/server/v1/team_pb2.pyi +56 -10
  158. chalk/_gen/chalk/server/v1/team_pb2_grpc.py +90 -0
  159. chalk/_gen/chalk/server/v1/team_pb2_grpc.pyi +24 -0
  160. chalk/_gen/chalk/server/v1/topic_pb2.py +5 -3
  161. chalk/_gen/chalk/server/v1/topic_pb2.pyi +10 -1
  162. chalk/_gen/chalk/server/v1/trace_pb2.py +50 -28
  163. chalk/_gen/chalk/server/v1/trace_pb2.pyi +121 -0
  164. chalk/_gen/chalk/server/v1/trace_pb2_grpc.py +135 -0
  165. chalk/_gen/chalk/server/v1/trace_pb2_grpc.pyi +42 -0
  166. chalk/_gen/chalk/server/v1/webhook_pb2.py +9 -3
  167. chalk/_gen/chalk/server/v1/webhook_pb2.pyi +18 -0
  168. chalk/_gen/chalk/server/v1/webhook_pb2_grpc.py +45 -0
  169. chalk/_gen/chalk/server/v1/webhook_pb2_grpc.pyi +12 -0
  170. chalk/_gen/chalk/streaming/v1/debug_service_pb2.py +62 -0
  171. chalk/_gen/chalk/streaming/v1/debug_service_pb2.pyi +75 -0
  172. chalk/_gen/chalk/streaming/v1/debug_service_pb2_grpc.py +221 -0
  173. chalk/_gen/chalk/streaming/v1/debug_service_pb2_grpc.pyi +88 -0
  174. chalk/_gen/chalk/streaming/v1/simple_streaming_service_pb2.py +19 -7
  175. chalk/_gen/chalk/streaming/v1/simple_streaming_service_pb2.pyi +96 -3
  176. chalk/_gen/chalk/streaming/v1/simple_streaming_service_pb2_grpc.py +48 -0
  177. chalk/_gen/chalk/streaming/v1/simple_streaming_service_pb2_grpc.pyi +20 -0
  178. chalk/_gen/chalk/utils/v1/field_change_pb2.py +32 -0
  179. chalk/_gen/chalk/utils/v1/field_change_pb2.pyi +42 -0
  180. chalk/_gen/chalk/utils/v1/field_change_pb2_grpc.py +4 -0
  181. chalk/_gen/chalk/utils/v1/field_change_pb2_grpc.pyi +4 -0
  182. chalk/_lsp/error_builder.py +11 -0
  183. chalk/_monitoring/Chart.py +1 -3
  184. chalk/_version.py +1 -1
  185. chalk/cli.py +5 -10
  186. chalk/client/client.py +178 -64
  187. chalk/client/client_async.py +154 -0
  188. chalk/client/client_async_impl.py +22 -0
  189. chalk/client/client_grpc.py +738 -112
  190. chalk/client/client_impl.py +541 -136
  191. chalk/client/dataset.py +27 -6
  192. chalk/client/models.py +99 -2
  193. chalk/client/serialization/model_serialization.py +126 -10
  194. chalk/config/project_config.py +1 -1
  195. chalk/df/LazyFramePlaceholder.py +1154 -0
  196. chalk/df/ast_parser.py +2 -10
  197. chalk/features/_class_property.py +7 -0
  198. chalk/features/_embedding/embedding.py +1 -0
  199. chalk/features/_embedding/sentence_transformer.py +1 -1
  200. chalk/features/_encoding/converter.py +83 -2
  201. chalk/features/_encoding/pyarrow.py +20 -4
  202. chalk/features/_encoding/rich.py +1 -3
  203. chalk/features/_tensor.py +1 -2
  204. chalk/features/dataframe/_filters.py +14 -5
  205. chalk/features/dataframe/_impl.py +91 -36
  206. chalk/features/dataframe/_validation.py +11 -7
  207. chalk/features/feature_field.py +40 -30
  208. chalk/features/feature_set.py +1 -2
  209. chalk/features/feature_set_decorator.py +1 -0
  210. chalk/features/feature_wrapper.py +42 -3
  211. chalk/features/hooks.py +81 -12
  212. chalk/features/inference.py +65 -10
  213. chalk/features/resolver.py +338 -56
  214. chalk/features/tag.py +1 -3
  215. chalk/features/underscore_features.py +2 -1
  216. chalk/functions/__init__.py +456 -21
  217. chalk/functions/holidays.py +1 -3
  218. chalk/gitignore/gitignore_parser.py +5 -1
  219. chalk/importer.py +186 -74
  220. chalk/ml/__init__.py +6 -2
  221. chalk/ml/model_hooks.py +368 -51
  222. chalk/ml/model_reference.py +68 -10
  223. chalk/ml/model_version.py +34 -21
  224. chalk/ml/utils.py +143 -40
  225. chalk/operators/_utils.py +14 -3
  226. chalk/parsed/_proto/export.py +22 -0
  227. chalk/parsed/duplicate_input_gql.py +4 -0
  228. chalk/parsed/expressions.py +1 -3
  229. chalk/parsed/json_conversions.py +21 -14
  230. chalk/parsed/to_proto.py +16 -4
  231. chalk/parsed/user_types_to_json.py +31 -10
  232. chalk/parsed/validation_from_registries.py +182 -0
  233. chalk/queries/named_query.py +16 -6
  234. chalk/queries/scheduled_query.py +13 -1
  235. chalk/serialization/parsed_annotation.py +25 -12
  236. chalk/sql/__init__.py +221 -0
  237. chalk/sql/_internal/integrations/athena.py +6 -1
  238. chalk/sql/_internal/integrations/bigquery.py +22 -2
  239. chalk/sql/_internal/integrations/databricks.py +61 -18
  240. chalk/sql/_internal/integrations/mssql.py +281 -0
  241. chalk/sql/_internal/integrations/postgres.py +11 -3
  242. chalk/sql/_internal/integrations/redshift.py +4 -0
  243. chalk/sql/_internal/integrations/snowflake.py +11 -2
  244. chalk/sql/_internal/integrations/util.py +2 -1
  245. chalk/sql/_internal/sql_file_resolver.py +55 -10
  246. chalk/sql/_internal/sql_source.py +36 -2
  247. chalk/streams/__init__.py +1 -3
  248. chalk/streams/_kafka_source.py +5 -1
  249. chalk/streams/_windows.py +16 -4
  250. chalk/streams/types.py +1 -2
  251. chalk/utils/__init__.py +1 -3
  252. chalk/utils/_otel_version.py +13 -0
  253. chalk/utils/async_helpers.py +14 -5
  254. chalk/utils/df_utils.py +2 -2
  255. chalk/utils/duration.py +1 -3
  256. chalk/utils/job_log_display.py +538 -0
  257. chalk/utils/missing_dependency.py +5 -4
  258. chalk/utils/notebook.py +255 -2
  259. chalk/utils/pl_helpers.py +190 -37
  260. chalk/utils/pydanticutil/pydantic_compat.py +1 -2
  261. chalk/utils/storage_client.py +246 -0
  262. chalk/utils/threading.py +1 -3
  263. chalk/utils/tracing.py +194 -86
  264. {chalkpy-2.89.22.dist-info → chalkpy-2.95.3.dist-info}/METADATA +53 -21
  265. {chalkpy-2.89.22.dist-info → chalkpy-2.95.3.dist-info}/RECORD +268 -198
  266. {chalkpy-2.89.22.dist-info → chalkpy-2.95.3.dist-info}/WHEEL +0 -0
  267. {chalkpy-2.89.22.dist-info → chalkpy-2.95.3.dist-info}/entry_points.txt +0 -0
  268. {chalkpy-2.89.22.dist-info → chalkpy-2.95.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,538 @@
1
+ """Job log display and state management for monitoring background jobs."""
2
+
3
+ import datetime as dt
4
+ import re
5
+ import time
6
+ from typing import TYPE_CHECKING, Callable, Optional
7
+
8
+ from google.protobuf import timestamp_pb2
9
+ from rich.columns import Columns
10
+ from rich.console import Console, Group
11
+ from rich.panel import Panel
12
+ from rich.spinner import Spinner
13
+ from rich.style import Style
14
+ from rich.table import Table
15
+ from rich.text import Text
16
+
17
+ from chalk._gen.chalk.server.v1.dataplanejobqueue_pb2 import GetJobQueueOperationSummaryResponse, JobQueueState
18
+ from chalk._gen.chalk.server.v1.log_pb2 import SearchLogEntriesRequest
19
+ from chalk._reporting.rich.color import (
20
+ CITRUSY_YELLOW,
21
+ GRASSY_GREEN,
22
+ SERENDIPITOUS_PURPLE,
23
+ SHADOWY_LAVENDER,
24
+ SHY_RED,
25
+ UNDERLYING_CYAN,
26
+ )
27
+ from chalk.utils.collections import FrozenOrderedSet
28
+
29
+ if TYPE_CHECKING:
30
+ from chalk._gen.chalk.server.v1.log_pb2_grpc import LogSearchServiceStub
31
+
32
+
33
+ class JobLogDisplay:
34
+ """Manages the display and state tracking for job monitoring.
35
+
36
+ This class provides a generic interface for monitoring background jobs with
37
+ status updates and log streaming. It can be used for any job type that uses
38
+ the job queue system (training jobs, data processing jobs, etc.).
39
+ """
40
+
41
+ def __init__(self, max_logs_display: int = 10, title: str = "Jobs"):
42
+ """Initialize the job log display.
43
+
44
+ Parameters
45
+ ----------
46
+ max_logs_display
47
+ Maximum number of recent logs to display
48
+ title
49
+ Title to display in the status table (e.g., "Model Training Jobs", "Processing Jobs")
50
+ """
51
+ super().__init__()
52
+ self.job_states: dict[int, tuple[str, JobQueueState]] = {}
53
+ self.recent_logs: list[tuple[str, str]] = []
54
+ self.seen_log_content: set[tuple[str, str]] = set()
55
+ self.max_logs_display = max_logs_display
56
+ self.animation_frame = 0
57
+ self.start_time = time.time()
58
+ self.console = Console()
59
+ self.title = title
60
+
61
+ # Log following state
62
+ self.latest_timestamp: Optional[timestamp_pb2.Timestamp] = None
63
+ self.seen_log_ids: dict[str, bool] = {}
64
+
65
+ # Terminal states that indicate the job has finished
66
+ self.terminal_states = FrozenOrderedSet(
67
+ {
68
+ JobQueueState.JOB_QUEUE_STATE_COMPLETED,
69
+ JobQueueState.JOB_QUEUE_STATE_FAILED,
70
+ JobQueueState.JOB_QUEUE_STATE_CANCELED,
71
+ }
72
+ )
73
+
74
+ def update_job_state(self, job_idx: int, state_name: str, state: JobQueueState) -> None:
75
+ """Update the state of a specific job.
76
+
77
+ Parameters
78
+ ----------
79
+ job_idx
80
+ Index of the job
81
+ state_name
82
+ Human-readable name of the state
83
+ state
84
+ The JobQueueState enum value
85
+ """
86
+ self.job_states[job_idx] = (state_name, state)
87
+
88
+ def add_log(self, timestamp: str, message: str) -> None:
89
+ """Add a log entry to the recent logs.
90
+
91
+ Parameters
92
+ ----------
93
+ timestamp
94
+ Formatted timestamp string
95
+ message
96
+ Log message
97
+ """
98
+ self.recent_logs.append((timestamp, message))
99
+
100
+ def is_all_terminal(self) -> bool:
101
+ """Check if all jobs have reached a terminal state.
102
+
103
+ Returns
104
+ -------
105
+ bool
106
+ True if all jobs are in a terminal state
107
+ """
108
+ if not self.job_states:
109
+ return False
110
+ return all(state in self.terminal_states for _, state in self.job_states.values())
111
+
112
+ def is_successful(self) -> bool:
113
+ """Check if all jobs completed successfully.
114
+
115
+ Returns
116
+ -------
117
+ bool
118
+ True if all jobs completed successfully
119
+ """
120
+ return all(state == JobQueueState.JOB_QUEUE_STATE_COMPLETED for _, state in self.job_states.values())
121
+
122
+ @staticmethod
123
+ def clean_log_message(message: str) -> str:
124
+ """Remove job metadata from log message.
125
+
126
+ Parameters
127
+ ----------
128
+ message
129
+ Raw log message
130
+
131
+ Returns
132
+ -------
133
+ str
134
+ Cleaned log message
135
+ """
136
+ # Remove patterns like "job(id=1, ... attempt_idx=1)"
137
+ cleaned = re.sub(r"job\(id=\d+.*?attempt_idx=\d+\)\s*", "", message)
138
+ return cleaned.strip()
139
+
140
+ def get_status_renderable(self, state: JobQueueState, state_name: str):
141
+ """Return the renderable (spinner or text) for a given job state.
142
+
143
+ Parameters
144
+ ----------
145
+ state
146
+ The JobQueueState enum value
147
+ state_name
148
+ Human-readable name of the state
149
+
150
+ Returns
151
+ -------
152
+ Text or Columns
153
+ Rich renderable for the status
154
+ """
155
+ display_name = state_name.replace("JOB_QUEUE_STATE_", "").replace("_", " ").title()
156
+
157
+ if state == JobQueueState.JOB_QUEUE_STATE_COMPLETED:
158
+ return Text(f"✓ {display_name}", style=Style(color=GRASSY_GREEN, bold=True))
159
+ elif state == JobQueueState.JOB_QUEUE_STATE_FAILED:
160
+ return Text(f"✗ {display_name}", style=Style(color=SHY_RED, bold=True))
161
+ elif state == JobQueueState.JOB_QUEUE_STATE_CANCELED:
162
+ return Text(f"⊗ {display_name}", style=Style(color=SHADOWY_LAVENDER))
163
+ elif "RUNNING" in state_name:
164
+ return Columns(
165
+ [
166
+ Spinner("dots", style=Style(color=UNDERLYING_CYAN)),
167
+ Text(f"{display_name}", style=Style(color=UNDERLYING_CYAN, bold=True)),
168
+ ],
169
+ expand=False,
170
+ )
171
+ elif "PENDING" in state_name or "QUEUED" in state_name:
172
+ return Columns(
173
+ [
174
+ Spinner("dots2", style=Style(color=CITRUSY_YELLOW)),
175
+ Text(f"{display_name}", style=Style(color=CITRUSY_YELLOW)),
176
+ ],
177
+ expand=False,
178
+ )
179
+ else:
180
+ return Text(f"◐ {display_name}", style=Style(color=SERENDIPITOUS_PURPLE))
181
+
182
+ def format_elapsed_time(self) -> str:
183
+ """Format elapsed time since job started.
184
+
185
+ Returns
186
+ -------
187
+ str
188
+ Formatted time string (HH:MM:SS or MM:SS)
189
+ """
190
+ elapsed = int(time.time() - self.start_time)
191
+ minutes, seconds = divmod(elapsed, 60)
192
+ hours, minutes = divmod(minutes, 60)
193
+ if hours > 0:
194
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
195
+ return f"{minutes:02d}:{seconds:02d}"
196
+
197
+ def create_status_table(self) -> Table:
198
+ """Create a status table showing current job states.
199
+
200
+ Returns
201
+ -------
202
+ Table
203
+ Rich table with job statuses
204
+ """
205
+ elapsed_str = self.format_elapsed_time()
206
+ title_text = Text(self.title, style=Style(color=UNDERLYING_CYAN, bold=True))
207
+ title_text.append(f" [{elapsed_str}]", style=Style(color=SHADOWY_LAVENDER, dim=True))
208
+
209
+ table = Table(
210
+ title=title_text,
211
+ title_justify="left",
212
+ box=None,
213
+ show_header=True,
214
+ header_style=Style(color=SHADOWY_LAVENDER, bold=True),
215
+ )
216
+ table.add_column("Job", style=Style(color=SHADOWY_LAVENDER))
217
+ table.add_column("Status")
218
+
219
+ if not self.job_states:
220
+ waiting_status = Columns(
221
+ [
222
+ Spinner("dots", style=Style(color=CITRUSY_YELLOW)),
223
+ Text("Waiting for jobs...", style=Style(color=CITRUSY_YELLOW, italic=True)),
224
+ ],
225
+ expand=False,
226
+ )
227
+ table.add_row("", waiting_status)
228
+ else:
229
+ for job_idx in sorted(self.job_states.keys()):
230
+ state_name, state = self.job_states[job_idx]
231
+ status_renderable = self.get_status_renderable(state, state_name)
232
+ table.add_row(f"Job {job_idx}", status_renderable)
233
+
234
+ return table
235
+
236
+ def create_logs_panel(self) -> Panel:
237
+ """Create a panel showing recent logs.
238
+
239
+ Returns
240
+ -------
241
+ Panel
242
+ Rich panel with recent log entries
243
+ """
244
+ if not self.recent_logs:
245
+ num_dots = (self.animation_frame // 2) % 4
246
+ dots = "." * num_dots
247
+ log_content = Text(f"Waiting for logs{dots:<3}", style=Style(color=SHADOWY_LAVENDER, italic=True))
248
+ else:
249
+ log_lines: list[Text] = []
250
+ for timestamp, message in self.recent_logs[-self.max_logs_display :]:
251
+ line = Text()
252
+ line.append(timestamp, style=Style(color=SERENDIPITOUS_PURPLE, bold=True))
253
+ line.append(" ")
254
+ cleaned_message = self.clean_log_message(message)
255
+ line.append(cleaned_message, style=Style(color="white"))
256
+ log_lines.append(line)
257
+ log_content = Text("\n").join(log_lines)
258
+
259
+ return Panel(
260
+ log_content,
261
+ title="Recent Logs",
262
+ title_align="left",
263
+ border_style=Style(color=SERENDIPITOUS_PURPLE),
264
+ )
265
+
266
+ def create_display(self) -> Group:
267
+ """Create the full display with status and logs.
268
+
269
+ Returns
270
+ -------
271
+ Group
272
+ Rich group containing status table and logs panel
273
+ """
274
+ return Group(self.create_status_table(), Text(""), self.create_logs_panel())
275
+
276
+ def increment_animation(self) -> None:
277
+ """Increment the animation frame counter."""
278
+ self.animation_frame += 1
279
+
280
+ def print_final_summary(self) -> None:
281
+ """Print the final summary of the job."""
282
+ if self.is_successful():
283
+ self.console.print(Text("✓ Job completed successfully", style=Style(color=GRASSY_GREEN, bold=True)))
284
+ else:
285
+ self.console.print(Text("✗ Job failed or was canceled", style=Style(color=SHY_RED, bold=True)))
286
+
287
+ def poll_logs(
288
+ self,
289
+ log_stub: "LogSearchServiceStub",
290
+ query: str,
291
+ poll_interval: float,
292
+ should_stop_callback: Callable[[], bool],
293
+ output_callback: Optional[Callable[[str, str], None]] = None,
294
+ ) -> None:
295
+ """Poll for new logs and display them.
296
+
297
+ Parameters
298
+ ----------
299
+ log_stub
300
+ The gRPC stub for log search service
301
+ query
302
+ The search query to filter logs
303
+ poll_interval
304
+ Time in seconds between polling for new logs
305
+ should_stop_callback
306
+ Callback that returns True when polling should stop
307
+ output_callback
308
+ Optional callback function that receives (timestamp, message) for each log entry.
309
+ If None, logs are added to the display.
310
+ """
311
+ try:
312
+ while not should_stop_callback():
313
+ # Fetch logs starting from the latest timestamp we've seen
314
+ req = SearchLogEntriesRequest(query=query)
315
+
316
+ if self.latest_timestamp is not None:
317
+ req.start_time.CopyFrom(self.latest_timestamp)
318
+
319
+ try:
320
+ resp = log_stub.SearchLogEntries(req)
321
+ except Exception as e:
322
+ if output_callback:
323
+ output_callback("", f"[LOG ERROR] {e}")
324
+ else:
325
+ self.add_log("", f"[LOG ERROR] {e}")
326
+ time.sleep(poll_interval)
327
+ continue
328
+
329
+ # Sort logs by timestamp (oldest first)
330
+ sorted_logs = sorted(
331
+ resp.log_entries, key=lambda log: log.timestamp.seconds + log.timestamp.nanos / 1e9
332
+ )
333
+
334
+ # Display new logs and track the latest timestamp
335
+ max_timestamp = self.latest_timestamp
336
+ for log in sorted_logs:
337
+ if log.id not in self.seen_log_ids:
338
+ formatted_time = self._format_timestamp(log.timestamp)
339
+ formatted_message = log.message.replace("\n", " ")
340
+
341
+ if output_callback:
342
+ output_callback(formatted_time, formatted_message)
343
+ else:
344
+ self.add_log(formatted_time, formatted_message)
345
+
346
+ self.seen_log_ids[log.id] = True
347
+
348
+ # Track the maximum timestamp we've seen
349
+ if max_timestamp is None or not self._is_timestamp_after(max_timestamp, log.timestamp):
350
+ max_timestamp = log.timestamp
351
+
352
+ # Update latest_timestamp after processing all logs in this batch
353
+ if max_timestamp is not None and (
354
+ self.latest_timestamp is None
355
+ or max_timestamp.seconds > self.latest_timestamp.seconds
356
+ or (
357
+ max_timestamp.seconds == self.latest_timestamp.seconds
358
+ and max_timestamp.nanos > self.latest_timestamp.nanos
359
+ )
360
+ ):
361
+ # Advance by 1 full second since server filters at second-level precision (RFC3339)
362
+ # Using nanosecond precision would cause the same logs to be re-fetched
363
+ self.latest_timestamp = timestamp_pb2.Timestamp(seconds=max_timestamp.seconds + 1, nanos=0)
364
+
365
+ # Wait before next poll
366
+ time.sleep(poll_interval)
367
+ except KeyboardInterrupt:
368
+ pass
369
+
370
+ @staticmethod
371
+ def _format_timestamp(timestamp: timestamp_pb2.Timestamp) -> str:
372
+ """Format a protobuf timestamp for display.
373
+
374
+ Parameters
375
+ ----------
376
+ timestamp
377
+ The protobuf timestamp to format
378
+
379
+ Returns
380
+ -------
381
+ str
382
+ Formatted timestamp string
383
+ """
384
+ dt_obj = dt.datetime.fromtimestamp(timestamp.seconds + timestamp.nanos / 1e9, tz=dt.timezone.utc)
385
+ return dt_obj.strftime("%Y-%m-%d %H:%M:%S")
386
+
387
+ @staticmethod
388
+ def _is_timestamp_after(ts1: timestamp_pb2.Timestamp, ts2: Optional[timestamp_pb2.Timestamp]) -> bool:
389
+ """Check if ts1 is after ts2.
390
+
391
+ Parameters
392
+ ----------
393
+ ts1
394
+ First timestamp
395
+ ts2
396
+ Second timestamp
397
+
398
+ Returns
399
+ -------
400
+ bool
401
+ True if ts1 is after ts2
402
+ """
403
+ if ts2 is None:
404
+ return True
405
+ if ts1.seconds > ts2.seconds:
406
+ return True
407
+ if ts1.seconds == ts2.seconds and ts1.nanos > ts2.nanos:
408
+ return True
409
+ return False
410
+
411
+ def poll_job_status(
412
+ self,
413
+ get_status_callback: Callable[[], "GetJobQueueOperationSummaryResponse"],
414
+ poll_interval: float,
415
+ should_stop_callback: Callable[[], bool],
416
+ ) -> None:
417
+ """Poll for job status updates.
418
+
419
+ Parameters
420
+ ----------
421
+ get_status_callback
422
+ Callback function that returns the job queue operation summary
423
+ poll_interval
424
+ Time in seconds between polling for status
425
+ should_stop_callback
426
+ Callback that returns True when polling should stop
427
+ """
428
+ try:
429
+ while not should_stop_callback():
430
+ try:
431
+ response = get_status_callback()
432
+
433
+ if response.HasField("summary"):
434
+ # Update job states
435
+ for row_summary in response.summary.indexed_row_summaries.values():
436
+ job_idx = row_summary.job_idx if row_summary.HasField("job_idx") else 0
437
+ state_name = JobQueueState.Name(row_summary.state)
438
+ self.update_job_state(job_idx, state_name, row_summary.state)
439
+
440
+ # Stop when all jobs reach terminal state
441
+ if self.is_all_terminal():
442
+ return
443
+
444
+ except Exception as e:
445
+ # Add error to logs
446
+ self.add_log("", f"[STATUS ERROR] {e}")
447
+
448
+ # Wait before next poll
449
+ time.sleep(poll_interval)
450
+ except KeyboardInterrupt:
451
+ pass
452
+
453
+ def follow_job(
454
+ self,
455
+ get_status_callback: Callable[[], "GetJobQueueOperationSummaryResponse"],
456
+ log_stub: "LogSearchServiceStub",
457
+ log_query: str,
458
+ poll_interval: float = 2.0,
459
+ output_callback: Optional[Callable[[str, str], None]] = None,
460
+ ) -> None:
461
+ """Follow a job, displaying both status and logs.
462
+
463
+ This method handles all the threading coordination and display logic
464
+ for following a job in real-time.
465
+
466
+ Parameters
467
+ ----------
468
+ get_status_callback
469
+ Callback function that returns the job queue operation summary
470
+ log_stub
471
+ The gRPC stub for log search service
472
+ log_query
473
+ The search query to filter logs
474
+ poll_interval
475
+ Time in seconds between polling for status and logs. Defaults to 2.0 seconds.
476
+ output_callback
477
+ Optional callback function that receives (timestamp, message) for each log entry.
478
+ If None, logs are displayed using Rich live display.
479
+ """
480
+ import threading
481
+
482
+ from rich.live import Live
483
+
484
+ # Flag to coordinate between threads
485
+ should_stop = threading.Event()
486
+
487
+ def status_done_callback():
488
+ should_stop.set()
489
+
490
+ def poll_status():
491
+ try:
492
+ self.poll_job_status(
493
+ get_status_callback=get_status_callback,
494
+ poll_interval=poll_interval,
495
+ should_stop_callback=should_stop.is_set,
496
+ )
497
+ status_done_callback()
498
+ except KeyboardInterrupt:
499
+ should_stop.set()
500
+
501
+ def poll_logs_thread():
502
+ self.poll_logs(
503
+ log_stub=log_stub,
504
+ query=log_query,
505
+ poll_interval=poll_interval,
506
+ should_stop_callback=should_stop.is_set,
507
+ output_callback=output_callback,
508
+ )
509
+
510
+ # Start both polling threads
511
+ status_thread = threading.Thread(target=poll_status, daemon=True)
512
+ log_thread = threading.Thread(target=poll_logs_thread, daemon=True)
513
+
514
+ status_thread.start()
515
+ log_thread.start()
516
+
517
+ # Use Live display if no callback is provided
518
+ if output_callback is None:
519
+ with Live(self.create_display(), console=self.console, refresh_per_second=4) as live:
520
+ try:
521
+ while not should_stop.is_set():
522
+ live.update(self.create_display())
523
+ self.increment_animation()
524
+ time.sleep(0.25)
525
+ except KeyboardInterrupt:
526
+ should_stop.set()
527
+
528
+ # Print final summary
529
+ self.print_final_summary()
530
+ else:
531
+ # When using callback, just wait for completion
532
+ try:
533
+ status_thread.join()
534
+ log_thread.join()
535
+ except KeyboardInterrupt:
536
+ should_stop.set()
537
+ status_thread.join(timeout=1)
538
+ log_thread.join(timeout=1)
@@ -5,7 +5,8 @@ class MissingDependencyException(ImportError):
5
5
  ...
6
6
 
7
7
 
8
- def missing_dependency_exception(name: str):
9
- return MissingDependencyException(
10
- f"Missing pip dependency '{name}' for chalkpy=={chalk.__version__}. Please add this to your requirements.txt file and pip install."
11
- )
8
+ def missing_dependency_exception(name: str, original_error: Exception | None = None):
9
+ msg = f"Missing pip dependency '{name}' for chalkpy=={chalk.__version__}. Please add this to your requirements.txt file and pip install."
10
+ if original_error:
11
+ msg += f"\n\n{original_error}"
12
+ return MissingDependencyException(msg)