chronaxis 0.1.0.dev0__py3-none-any.whl → 0.1.0.dev2__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 (590) hide show
  1. chronaxis/__init__.py +4 -4
  2. chronaxis/__main__.py +1 -1
  3. chronaxis/_batch_worker.py +69 -0
  4. chronaxis/analysis_cli.py +14 -6
  5. chronaxis/batch/api.py +11 -4
  6. chronaxis/batch/cli.py +1 -1
  7. chronaxis/batch/models.py +29 -1
  8. chronaxis/batch/process_transport.py +69 -63
  9. chronaxis/batch/publication.py +35 -15
  10. chronaxis/batch/runner.py +33 -31
  11. chronaxis/can_dependencies.py +1 -1
  12. chronaxis/cli.py +35 -84
  13. chronaxis/configuration/diagnostics.py +9 -0
  14. chronaxis/configuration/file_tools.py +12 -18
  15. chronaxis/configuration/imports.py +6 -6
  16. chronaxis/configuration/origins.py +7 -7
  17. chronaxis/configuration/resolution.py +48 -15
  18. chronaxis/configuration/templates/batch/analysis-annotated.yaml +1 -1
  19. chronaxis/configuration/templates/batch/analysis-minimal.yaml +1 -1
  20. chronaxis/configuration/templates/can-asc/annotated.yaml +1 -1
  21. chronaxis/configuration/templates/can-asc/minimal.yaml +1 -1
  22. chronaxis/configuration/templates/can-blf/annotated.yaml +1 -1
  23. chronaxis/configuration/templates/can-blf/minimal.yaml +1 -1
  24. chronaxis/configuration/templates/can-export/annotated.yaml +1 -1
  25. chronaxis/configuration/templates/can-export/minimal.yaml +1 -1
  26. chronaxis/configuration/templates/catalog.json +0 -21
  27. chronaxis/configuration/templates/charge-session/annotated.yaml +1 -1
  28. chronaxis/configuration/templates/charge-session/minimal.yaml +1 -1
  29. chronaxis/configuration/templates/command-response/annotated.yaml +1 -1
  30. chronaxis/configuration/templates/command-response/minimal.yaml +1 -1
  31. chronaxis/configuration/templates/fault-window/annotated.yaml +1 -1
  32. chronaxis/configuration/templates/fault-window/minimal.yaml +1 -1
  33. chronaxis/configuration/templates/raw-can-csv/annotated.yaml +1 -1
  34. chronaxis/configuration/templates/raw-can-csv/minimal.yaml +1 -1
  35. chronaxis/configuration/templates/signal-csv/annotated.yaml +1 -1
  36. chronaxis/configuration/templates/signal-csv/minimal.yaml +1 -1
  37. chronaxis/configuration/templates/signal-export/annotated.yaml +1 -1
  38. chronaxis/configuration/templates/signal-export/minimal.yaml +1 -1
  39. chronaxis/configuration/templates/stability/annotated.yaml +3 -1
  40. chronaxis/configuration/templates/stability/minimal.yaml +3 -1
  41. chronaxis/configuration/templates/threshold/annotated.yaml +1 -1
  42. chronaxis/configuration/templates/threshold/minimal.yaml +1 -1
  43. chronaxis/core/__init__.py +2 -1
  44. chronaxis/core/affine.py +79 -0
  45. chronaxis/core/affine_numpy.py +84 -0
  46. chronaxis/core/binding.py +68 -5
  47. chronaxis/core/calculation_output_numpy.py +29 -0
  48. chronaxis/core/calculation_output_reference.py +27 -0
  49. chronaxis/core/calculations.py +134 -51
  50. chronaxis/core/calculations_memory_numpy.py +17 -9
  51. chronaxis/core/calculations_memory_reference.py +34 -8
  52. chronaxis/core/calculations_numpy.py +47 -30
  53. chronaxis/core/calculations_reference.py +41 -32
  54. chronaxis/core/conditional_time_reduction_numpy.py +25 -19
  55. chronaxis/core/conditional_time_reduction_reference.py +25 -17
  56. chronaxis/core/counts_v4.py +34 -0
  57. chronaxis/core/declarations.py +25 -0
  58. chronaxis/core/decoded_binding.py +17 -0
  59. chronaxis/core/enum_codes.py +223 -0
  60. chronaxis/core/expression.py +104 -27
  61. chronaxis/core/expression_integer_numpy.py +60 -0
  62. chronaxis/core/expression_integer_reference.py +46 -0
  63. chronaxis/core/expression_integer_v4.py +97 -0
  64. chronaxis/core/expression_math_numpy.py +47 -0
  65. chronaxis/core/expression_math_reference.py +49 -0
  66. chronaxis/core/expression_math_v4.py +148 -0
  67. chronaxis/core/expression_numpy.py +70 -4
  68. chronaxis/core/expression_reference.py +42 -4
  69. chronaxis/core/expression_v4.py +302 -0
  70. chronaxis/core/extension_points.py +38 -0
  71. chronaxis/core/function_contracts.py +74 -0
  72. chronaxis/core/lookup.py +163 -0
  73. chronaxis/core/lookup_compiler.py +47 -0
  74. chronaxis/core/lookup_numpy.py +59 -0
  75. chronaxis/core/lookup_reference.py +50 -0
  76. chronaxis/core/message_binding.py +42 -2
  77. chronaxis/core/models.py +1 -0
  78. chronaxis/core/moments.py +68 -0
  79. chronaxis/core/moments_numpy.py +87 -0
  80. chronaxis/core/moments_reference.py +55 -0
  81. chronaxis/core/normalize_numpy.py +4 -2
  82. chronaxis/core/numeric.py +12 -3
  83. chronaxis/core/numeric_numpy.py +14 -0
  84. chronaxis/core/quantile_numpy.py +33 -0
  85. chronaxis/core/quantile_reference.py +31 -0
  86. chronaxis/core/quantiles.py +198 -0
  87. chronaxis/core/rate_numpy.py +93 -0
  88. chronaxis/core/rate_reference.py +69 -0
  89. chronaxis/core/rate_transforms.py +105 -0
  90. chronaxis/core/rolling_values.py +76 -0
  91. chronaxis/core/sample_reduction.py +15 -7
  92. chronaxis/core/sample_reduction_numpy.py +46 -21
  93. chronaxis/core/sample_reduction_reference.py +50 -20
  94. chronaxis/core/selection.py +22 -6
  95. chronaxis/core/sequence_reference.py +4 -2
  96. chronaxis/core/state.py +17 -6
  97. chronaxis/core/state_numpy.py +14 -4
  98. chronaxis/core/temporal_memory_numpy.py +25 -8
  99. chronaxis/core/temporal_memory_reference.py +24 -13
  100. chronaxis/core/time_reduction.py +5 -4
  101. chronaxis/core/time_reduction_numpy.py +25 -19
  102. chronaxis/core/time_reduction_reference.py +25 -17
  103. chronaxis/core/types.py +4 -0
  104. chronaxis/core/unit_conversion.py +104 -0
  105. chronaxis/core/unit_registry.py +567 -0
  106. chronaxis/core/units.py +1 -60
  107. chronaxis/core/where_updates.py +114 -0
  108. chronaxis/core/window_delta.py +2 -0
  109. chronaxis/core/windows.py +1 -1
  110. chronaxis/execution/calculation_blocks.py +4 -5
  111. chronaxis/execution/calculation_values.py +3 -1
  112. chronaxis/execution/condition_evidence.py +6 -3
  113. chronaxis/execution/current_message_calculation.py +30 -5
  114. chronaxis/execution/driver_expression.py +126 -34
  115. chronaxis/execution/expressions.py +91 -9
  116. chronaxis/execution/historical_conditional_blocks.py +4 -5
  117. chronaxis/execution/historical_conditional_time_calculation.py +4 -3
  118. chronaxis/execution/historical_sample_calculation.py +6 -2
  119. chronaxis/execution/historical_selection_rows.py +4 -4
  120. chronaxis/execution/historical_time_calculation.py +4 -3
  121. chronaxis/execution/history.py +17 -2
  122. chronaxis/execution/hosted_sample_reduction.py +62 -0
  123. chronaxis/execution/record_delta_condition.py +13 -4
  124. chronaxis/execution/record_event_durations.py +9 -5
  125. chronaxis/execution/record_integral.py +7 -0
  126. chronaxis/execution/record_object_counts.py +34 -0
  127. chronaxis/execution/record_rule_timeline.py +56 -10
  128. chronaxis/execution/record_scope_calculations.py +34 -13
  129. chronaxis/execution/record_sequences.py +2 -1
  130. chronaxis/execution/record_session_calculations.py +103 -28
  131. chronaxis/execution/record_time_calculation.py +45 -11
  132. chronaxis/execution/record_value_history.py +5 -4
  133. chronaxis/execution/record_windows.py +5 -1
  134. chronaxis/execution/required_expression.py +3 -7
  135. chronaxis/execution/result_aggregations.py +270 -0
  136. chronaxis/execution/run_session.py +6 -4
  137. chronaxis/execution/state.py +37 -3
  138. chronaxis/execution/transform_partial.py +43 -0
  139. chronaxis/execution/transform_stream.py +321 -0
  140. chronaxis/execution/transform_window.py +84 -0
  141. chronaxis/execution/where_state.py +76 -0
  142. chronaxis/execution/window_replay.py +6 -1
  143. chronaxis/extension_runtime.py +279 -0
  144. chronaxis/extensions.py +188 -0
  145. chronaxis/fast/__init__.py +3 -0
  146. chronaxis/fast/__main__.py +13 -0
  147. chronaxis/fast/baseline.py +22 -0
  148. chronaxis/fast/batch.py +90 -0
  149. chronaxis/fast/can_io.py +168 -0
  150. chronaxis/fast/cli.py +62 -0
  151. chronaxis/fast/compute.py +204 -0
  152. chronaxis/fast/config.py +225 -0
  153. chronaxis/fast/run.py +58 -0
  154. chronaxis/fast/totals.py +92 -0
  155. chronaxis/fast/worker.py +56 -0
  156. chronaxis/guide/README.md +23 -5
  157. chronaxis/guide/api/AnalysisPlan.md +64 -7
  158. chronaxis/guide/api/AnalysisResult.md +31 -4
  159. chronaxis/guide/api/AnalysisRunner.md +3 -2
  160. chronaxis/guide/api/BatchJobResult.md +2 -1
  161. chronaxis/guide/api/BatchReport.md +4 -2
  162. chronaxis/guide/api/BatchRunner.md +2 -2
  163. chronaxis/guide/api/ExportManifest.md +20 -0
  164. chronaxis/guide/api/ExtensionRegistry.md +32 -0
  165. chronaxis/guide/api/FunctionSpec.md +45 -0
  166. chronaxis/guide/api/OutputManifest.md +1 -1
  167. chronaxis/guide/api/PlanValidationError.md +56 -4
  168. chronaxis/guide/api/ReductionSpec.md +47 -0
  169. chronaxis/guide/api/RunMetadata.md +5 -4
  170. chronaxis/guide/api/TransformSpec.md +48 -0
  171. chronaxis/guide/api/fast.batch.run_batch.md +34 -0
  172. chronaxis/guide/api/fast.run.md +31 -0
  173. chronaxis/guide/api/index.json +193 -77
  174. chronaxis/guide/api/load_analysis_plan.md +2 -1
  175. chronaxis/guide/api/run_batch.md +2 -10
  176. chronaxis/guide/en/README.md +22 -4
  177. chronaxis/guide/en/api/AnalysisPlan.md +47 -23
  178. chronaxis/guide/en/api/AnalysisResult.md +30 -24
  179. chronaxis/guide/en/api/AnalysisRunner.md +11 -12
  180. chronaxis/guide/en/api/BatchJob.md +4 -4
  181. chronaxis/guide/en/api/BatchJobResult.md +21 -20
  182. chronaxis/guide/en/api/BatchPlan.md +10 -10
  183. chronaxis/guide/en/api/BatchReport.md +13 -11
  184. chronaxis/guide/en/api/BatchRunner.md +11 -11
  185. chronaxis/guide/en/api/ExportManifest.md +25 -0
  186. chronaxis/guide/en/api/ExtensionRegistry.md +32 -0
  187. chronaxis/guide/en/api/FunctionSpec.md +45 -0
  188. chronaxis/guide/en/api/OutputManifest.md +5 -5
  189. chronaxis/guide/en/api/PlanFragment.md +7 -7
  190. chronaxis/guide/en/api/PlanValidationError.md +36 -10
  191. chronaxis/guide/en/api/PresetSpec.md +10 -10
  192. chronaxis/guide/en/api/ReductionSpec.md +47 -0
  193. chronaxis/guide/en/api/RunMetadata.md +12 -13
  194. chronaxis/guide/en/api/TimeConversion.md +10 -10
  195. chronaxis/guide/en/api/TransformSpec.md +48 -0
  196. chronaxis/guide/en/api/build_preset.md +4 -4
  197. chronaxis/guide/en/api/checked_add_ns.md +4 -4
  198. chronaxis/guide/en/api/convert_time.md +4 -4
  199. chronaxis/guide/en/api/explain_analysis_plan.md +3 -3
  200. chronaxis/guide/en/api/fast.batch.run_batch.md +34 -0
  201. chronaxis/guide/en/api/fast.run.md +31 -0
  202. chronaxis/guide/en/api/index.json +193 -77
  203. chronaxis/guide/en/api/load_analysis_plan.md +9 -4
  204. chronaxis/guide/en/api/load_batch_plan.md +3 -3
  205. chronaxis/guide/en/api/parse_datetime_ns.md +4 -4
  206. chronaxis/guide/en/api/parse_duration.md +3 -3
  207. chronaxis/guide/en/api/resolve_preset_roles.md +4 -4
  208. chronaxis/guide/en/api/run_batch.md +10 -10
  209. chronaxis/guide/en/api/shift_times_reference.md +4 -4
  210. chronaxis/guide/en/api/time_deltas_reference.md +3 -3
  211. chronaxis/guide/en/api/validate_analysis_plan.md +3 -3
  212. chronaxis/guide/en/fields/analysis/aggregations-all-1.json +359 -0
  213. chronaxis/guide/en/fields/analysis/aggregations-all-2.json +301 -0
  214. chronaxis/guide/en/fields/analysis/aggregations-all-3.json +322 -0
  215. chronaxis/guide/en/fields/analysis/aggregations-all-4.json +169 -0
  216. chronaxis/guide/en/fields/analysis/calculations-all-1.json +14 -2
  217. chronaxis/guide/en/fields/analysis/calculations-all-2.json +55 -53
  218. chronaxis/guide/en/fields/analysis/calculations-all-3.json +207 -0
  219. chronaxis/guide/en/fields/{mapping/analysis-all-63.json → analysis/calculations-all-4.json} +154 -162
  220. chronaxis/guide/en/fields/{mapping/analysis-all-62.json → analysis/calculations-all-5.json} +129 -137
  221. chronaxis/guide/en/fields/analysis/calculations-all-6.json +172 -0
  222. chronaxis/guide/en/fields/analysis/config_version-all-1.json +2 -2
  223. chronaxis/guide/en/fields/analysis/lookups-all-1.json +196 -0
  224. chronaxis/guide/en/fields/analysis/outputs-all-1.json +1 -1
  225. chronaxis/guide/en/fields/analysis/presets-charge_session-1.json +1 -1
  226. chronaxis/guide/en/fields/analysis/presets-command_response-1.json +1 -1
  227. chronaxis/guide/en/fields/analysis/presets-common-1.json +20 -0
  228. chronaxis/guide/en/fields/analysis/presets-fault_context-1.json +1 -1
  229. chronaxis/guide/en/fields/analysis/presets-threshold_excursion-1.json +1 -1
  230. chronaxis/guide/en/fields/analysis/rules-common-1.json +20 -0
  231. chronaxis/guide/en/fields/analysis/signals-common-1.json +19 -0
  232. chronaxis/guide/en/fields/{mapping/analysis-all-80.json → analysis/signals-conditions-1.json} +153 -138
  233. chronaxis/guide/en/fields/analysis/signals-conditions-2.json +129 -0
  234. chronaxis/guide/en/fields/analysis/signals-variant-1-2.json +13 -28
  235. chronaxis/guide/en/fields/analysis/signals-variant-1-3.json +40 -29
  236. chronaxis/guide/en/fields/analysis/signals-variant-1-4.json +19 -0
  237. chronaxis/guide/en/fields/analysis/signals-variant-2-2.json +21 -23
  238. chronaxis/guide/en/fields/analysis/signals-variant-2-3.json +28 -30
  239. chronaxis/guide/en/fields/analysis/signals-variant-2-4.json +20 -19
  240. chronaxis/guide/en/fields/analysis/signals-variant-2-5.json +19 -0
  241. chronaxis/guide/en/fields/analysis/sources-common-1.json +20 -0
  242. chronaxis/guide/en/fields/analysis/sources-conditions-1.json +110 -0
  243. chronaxis/guide/en/fields/analysis/transforms-all-1.json +333 -0
  244. chronaxis/guide/en/fields/analysis/transforms-all-2.json +306 -0
  245. chronaxis/guide/en/fields/analysis/transforms-all-3.json +55 -0
  246. chronaxis/guide/en/fields/definitions/signals-all-2.json +16 -18
  247. chronaxis/guide/en/fields/definitions/signals-all-3.json +23 -22
  248. chronaxis/guide/en/fields/definitions/signals-all-4.json +12 -53
  249. chronaxis/guide/en/fields/definitions/signals-all-5.json +70 -52
  250. chronaxis/guide/en/fields/definitions/signals-all-6.json +57 -58
  251. chronaxis/guide/en/fields/definitions/signals-all-7.json +48 -0
  252. chronaxis/guide/en/fields/index.json +262 -1364
  253. chronaxis/guide/en/semantics.md +22 -7
  254. chronaxis/guide/en/tasks.json +234 -5
  255. chronaxis/guide/en/topics/aggregation-results.md +10 -0
  256. chronaxis/guide/en/topics/aggregations.md +62 -0
  257. chronaxis/guide/en/topics/expressions.md +249 -0
  258. chronaxis/guide/en/topics/fast-energy.md +22 -0
  259. chronaxis/guide/en/topics/integrators.md +8 -0
  260. chronaxis/guide/en/topics/message-calculations.md +95 -0
  261. chronaxis/guide/en/topics/message-example.md +10 -0
  262. chronaxis/guide/en/topics/power-statistics-results.md +43 -0
  263. chronaxis/guide/en/topics/power-statistics.md +29 -0
  264. chronaxis/guide/en/workflow.md +10 -2
  265. chronaxis/guide/examples/analysis-imports.yaml +1 -1
  266. chronaxis/guide/examples/analysis.yaml +1 -1
  267. chronaxis/guide/examples/conditional-energy.yaml +1 -1
  268. chronaxis/guide/examples/data/can/power-statistics.asc +7 -0
  269. chronaxis/guide/examples/example_support.py +22 -0
  270. chronaxis/guide/examples/execution-comparison.json +85 -0
  271. chronaxis/guide/examples/export-can-minimal.yaml +1 -1
  272. chronaxis/guide/examples/fast/analysis-blf.yaml +22 -0
  273. chronaxis/guide/examples/fast/analysis.yaml +22 -0
  274. chronaxis/guide/examples/fast/batch.yaml +5 -0
  275. chronaxis/guide/examples/fast/data/power.dbc +6 -0
  276. chronaxis/guide/examples/fast/data/run10.asc +4 -0
  277. chronaxis/guide/examples/fast/data/run2.asc +5 -0
  278. chronaxis/guide/examples/fast/data/single.blf +0 -0
  279. chronaxis/guide/examples/fast/unsupported.yaml +24 -0
  280. chronaxis/guide/examples/input-csv-minimal.yaml +2 -2
  281. chronaxis/guide/examples/lookup-power.yaml +27 -0
  282. chronaxis/guide/examples/message-directions.yaml +57 -0
  283. chronaxis/guide/examples/message-energy.yaml +40 -0
  284. chronaxis/guide/examples/message-minimal.yaml +28 -0
  285. chronaxis/guide/examples/power-statistics.yaml +83 -0
  286. chronaxis/guide/examples/preset-charge-energy.yaml +2 -2
  287. chronaxis/guide/examples/preset-charge-minimal.yaml +1 -1
  288. chronaxis/guide/examples/preset-fault-context.yaml +2 -2
  289. chronaxis/guide/examples/quality.yaml +1 -1
  290. chronaxis/guide/examples/readability_examples.py +203 -0
  291. chronaxis/guide/examples/results/conditional-energy-yaml//321/204/342/225/221/320/233/321/204/342/225/227/342/225/242/321/207/342/225/227/320/243/321/206/320/256/320/254.csv +2 -0
  292. chronaxis/guide/examples/results/power-statistics-yaml/result-summary.json +76 -0
  293. chronaxis/guide/examples/results/power-statistics-yaml//321/204/342/225/221/320/233/321/204/342/225/227/342/225/242/321/207/342/225/227/320/243/321/206/320/256/320/254.csv +5 -0
  294. chronaxis/guide/examples/results/power-statistics-yaml//321/204/342/225/235/320/252/321/210/320/277/320/255/321/207/342/225/227/320/243/321/206/320/256/320/254.csv +2 -0
  295. chronaxis/guide/examples/results/power-statistics-yaml//321/207/320/272/320/247/321/205/320/237/320/263/321/207/342/225/227/320/243/321/206/320/256/320/254.csv +2 -0
  296. chronaxis/guide/examples/results/power-statistics-yaml//321/210/320/221/320/252/321/205/320/240/320/230/321/207/342/225/227/320/243/321/206/320/256/320/254.csv +2 -0
  297. chronaxis/guide/examples/sequence-outcomes.yaml +1 -1
  298. chronaxis/guide/examples/signal-enum-minimal.yaml +1 -1
  299. chronaxis/guide/examples/signal-number-conversion.yaml +2 -3
  300. chronaxis/guide/examples/temporal/analysis.yaml +81 -0
  301. chronaxis/guide/examples/temporal/host-analysis.yaml +34 -0
  302. chronaxis/guide/examples/temporal/host_extensions.py +119 -0
  303. chronaxis/guide/examples/temporal/meter.asc +8 -0
  304. chronaxis/guide/examples/temporal/meter.dbc +9 -0
  305. chronaxis/guide/examples/temporal/rolling-actual.yaml +22 -0
  306. chronaxis/guide/examples/window-merged.yaml +1 -1
  307. chronaxis/guide/fields/analysis/aggregations-all-1.json +359 -0
  308. chronaxis/guide/fields/analysis/aggregations-all-2.json +301 -0
  309. chronaxis/guide/fields/analysis/aggregations-all-3.json +322 -0
  310. chronaxis/guide/fields/analysis/aggregations-all-4.json +169 -0
  311. chronaxis/guide/fields/analysis/calculations-all-1.json +14 -17
  312. chronaxis/guide/fields/analysis/calculations-all-2.json +76 -56
  313. chronaxis/guide/fields/analysis/calculations-all-3.json +222 -0
  314. chronaxis/guide/fields/{mapping/analysis-all-62.json → analysis/calculations-all-4.json} +147 -119
  315. chronaxis/guide/fields/analysis/calculations-all-5.json +310 -0
  316. chronaxis/guide/fields/analysis/calculations-all-6.json +138 -0
  317. chronaxis/guide/fields/analysis/config_version-all-1.json +2 -2
  318. chronaxis/guide/fields/analysis/lookups-all-1.json +196 -0
  319. chronaxis/guide/fields/analysis/outputs-all-1.json +1 -1
  320. chronaxis/guide/fields/analysis/presets-charge_session-1.json +1 -1
  321. chronaxis/guide/fields/analysis/presets-command_response-1.json +1 -1
  322. chronaxis/guide/fields/analysis/presets-common-1.json +20 -0
  323. chronaxis/guide/fields/analysis/presets-fault_context-1.json +1 -1
  324. chronaxis/guide/fields/analysis/presets-threshold_excursion-1.json +1 -1
  325. chronaxis/guide/fields/analysis/rules-common-1.json +20 -0
  326. chronaxis/guide/fields/analysis/signals-common-1.json +19 -0
  327. chronaxis/guide/fields/{mapping/analysis-all-79.json → analysis/signals-conditions-1.json} +153 -138
  328. chronaxis/guide/fields/analysis/signals-conditions-2.json +129 -0
  329. chronaxis/guide/fields/analysis/signals-variant-1-2.json +19 -15
  330. chronaxis/guide/fields/analysis/signals-variant-1-3.json +21 -27
  331. chronaxis/guide/fields/analysis/signals-variant-1-4.json +17 -0
  332. chronaxis/guide/fields/analysis/signals-variant-2-2.json +21 -23
  333. chronaxis/guide/fields/analysis/signals-variant-2-3.json +28 -28
  334. chronaxis/guide/fields/analysis/signals-variant-2-4.json +18 -35
  335. chronaxis/guide/fields/analysis/signals-variant-2-5.json +44 -0
  336. chronaxis/guide/fields/analysis/sources-common-1.json +20 -0
  337. chronaxis/guide/fields/analysis/sources-conditions-1.json +110 -0
  338. chronaxis/guide/fields/analysis/transforms-all-1.json +333 -0
  339. chronaxis/guide/fields/analysis/transforms-all-2.json +306 -0
  340. chronaxis/guide/fields/analysis/transforms-all-3.json +55 -0
  341. chronaxis/guide/fields/definitions/signals-all-2.json +16 -18
  342. chronaxis/guide/fields/definitions/signals-all-3.json +23 -25
  343. chronaxis/guide/fields/definitions/signals-all-4.json +15 -20
  344. chronaxis/guide/fields/definitions/signals-all-5.json +37 -34
  345. chronaxis/guide/fields/definitions/signals-all-6.json +39 -39
  346. chronaxis/guide/fields/definitions/signals-all-7.json +29 -0
  347. chronaxis/guide/fields/index.json +271 -1352
  348. chronaxis/guide/lookup.py +3 -2
  349. chronaxis/guide/manifest.json +216 -289
  350. chronaxis/guide/run_analysis.py +4 -3
  351. chronaxis/guide/schemas/analysis.schema.json +797 -34
  352. chronaxis/guide/semantics.md +24 -7
  353. chronaxis/guide/tasks.json +234 -5
  354. chronaxis/guide/topics/aggregation-results.md +10 -0
  355. chronaxis/guide/topics/aggregations.md +62 -0
  356. chronaxis/guide/topics/expressions.md +272 -0
  357. chronaxis/guide/topics/fast-energy.md +22 -0
  358. chronaxis/guide/topics/integrators.md +8 -0
  359. chronaxis/guide/topics/message-calculations.md +93 -0
  360. chronaxis/guide/topics/message-example.md +10 -0
  361. chronaxis/guide/topics/power-statistics-results.md +43 -0
  362. chronaxis/guide/topics/power-statistics.md +29 -0
  363. chronaxis/guide/workflow.md +10 -2
  364. chronaxis/planning/__init__.py +5 -2
  365. chronaxis/planning/aggregations.py +115 -0
  366. chronaxis/planning/channels.py +7 -4
  367. chronaxis/planning/checks.py +27 -0
  368. chronaxis/planning/config_groups.py +69 -53
  369. chronaxis/planning/dbc_defaults.py +125 -9
  370. chronaxis/planning/diagnostic_collection.py +358 -0
  371. chronaxis/planning/input_defaults.py +5 -0
  372. chronaxis/planning/input_requirements.py +14 -5
  373. chronaxis/planning/inputs.py +241 -211
  374. chronaxis/planning/intervals.py +12 -1
  375. chronaxis/planning/lookups.py +33 -0
  376. chronaxis/planning/message_calculations.py +71 -62
  377. chronaxis/planning/models.py +4 -1
  378. chronaxis/planning/nodes.py +269 -199
  379. chronaxis/planning/plan.py +190 -79
  380. chronaxis/planning/record_execution.py +16 -3
  381. chronaxis/planning/reports.py +83 -5
  382. chronaxis/planning/sequences.py +17 -9
  383. chronaxis/planning/signal_groups.py +18 -11
  384. chronaxis/planning/transforms.py +176 -0
  385. chronaxis/results/__init__.py +3 -3
  386. chronaxis/results/aggregation_row.py +78 -0
  387. chronaxis/results/codec.py +16 -1
  388. chronaxis/results/export_files.py +64 -19
  389. chronaxis/results/frame_exports.py +4 -3
  390. chronaxis/results/manifest.py +35 -5
  391. chronaxis/results/publication.py +131 -0
  392. chronaxis/results/readable.py +12 -2
  393. chronaxis/results/readable_format.py +1 -0
  394. chronaxis/results/result.py +54 -21
  395. chronaxis/results/rows.py +5 -2
  396. chronaxis/results/signal_exports.py +7 -4
  397. chronaxis/results/spool.py +3 -3
  398. chronaxis/results/summary.py +30 -2
  399. chronaxis/runner.py +13 -1
  400. chronaxis/schemas/analysis.schema.json +9214 -0
  401. chronaxis/schemas/batch-v1.schema.json +172 -0
  402. chronaxis/sources/models.py +4 -2
  403. chronaxis/sources/packing.py +5 -2
  404. {chronaxis-0.1.0.dev0.dist-info → chronaxis-0.1.0.dev2.dist-info}/METADATA +26 -26
  405. {chronaxis-0.1.0.dev0.dist-info → chronaxis-0.1.0.dev2.dist-info}/RECORD +409 -436
  406. chronaxis/configuration/templates/ini-mapping/annotated.yaml +0 -20
  407. chronaxis/configuration/templates/ini-mapping/minimal.yaml +0 -20
  408. chronaxis/configuration/templates/shared/source.ini +0 -5
  409. chronaxis/guide/en/fields/mapping/aliases-all-1.json +0 -91
  410. chronaxis/guide/en/fields/mapping/analysis-all-1.json +0 -382
  411. chronaxis/guide/en/fields/mapping/analysis-all-10.json +0 -301
  412. chronaxis/guide/en/fields/mapping/analysis-all-11.json +0 -312
  413. chronaxis/guide/en/fields/mapping/analysis-all-12.json +0 -311
  414. chronaxis/guide/en/fields/mapping/analysis-all-13.json +0 -314
  415. chronaxis/guide/en/fields/mapping/analysis-all-14.json +0 -310
  416. chronaxis/guide/en/fields/mapping/analysis-all-15.json +0 -297
  417. chronaxis/guide/en/fields/mapping/analysis-all-16.json +0 -226
  418. chronaxis/guide/en/fields/mapping/analysis-all-17.json +0 -230
  419. chronaxis/guide/en/fields/mapping/analysis-all-18.json +0 -227
  420. chronaxis/guide/en/fields/mapping/analysis-all-19.json +0 -230
  421. chronaxis/guide/en/fields/mapping/analysis-all-2.json +0 -323
  422. chronaxis/guide/en/fields/mapping/analysis-all-20.json +0 -221
  423. chronaxis/guide/en/fields/mapping/analysis-all-21.json +0 -237
  424. chronaxis/guide/en/fields/mapping/analysis-all-22.json +0 -233
  425. chronaxis/guide/en/fields/mapping/analysis-all-23.json +0 -222
  426. chronaxis/guide/en/fields/mapping/analysis-all-24.json +0 -231
  427. chronaxis/guide/en/fields/mapping/analysis-all-25.json +0 -225
  428. chronaxis/guide/en/fields/mapping/analysis-all-26.json +0 -230
  429. chronaxis/guide/en/fields/mapping/analysis-all-27.json +0 -228
  430. chronaxis/guide/en/fields/mapping/analysis-all-28.json +0 -230
  431. chronaxis/guide/en/fields/mapping/analysis-all-29.json +0 -223
  432. chronaxis/guide/en/fields/mapping/analysis-all-3.json +0 -318
  433. chronaxis/guide/en/fields/mapping/analysis-all-30.json +0 -233
  434. chronaxis/guide/en/fields/mapping/analysis-all-31.json +0 -218
  435. chronaxis/guide/en/fields/mapping/analysis-all-32.json +0 -239
  436. chronaxis/guide/en/fields/mapping/analysis-all-33.json +0 -233
  437. chronaxis/guide/en/fields/mapping/analysis-all-34.json +0 -225
  438. chronaxis/guide/en/fields/mapping/analysis-all-35.json +0 -228
  439. chronaxis/guide/en/fields/mapping/analysis-all-36.json +0 -226
  440. chronaxis/guide/en/fields/mapping/analysis-all-37.json +0 -244
  441. chronaxis/guide/en/fields/mapping/analysis-all-38.json +0 -326
  442. chronaxis/guide/en/fields/mapping/analysis-all-39.json +0 -299
  443. chronaxis/guide/en/fields/mapping/analysis-all-4.json +0 -297
  444. chronaxis/guide/en/fields/mapping/analysis-all-40.json +0 -311
  445. chronaxis/guide/en/fields/mapping/analysis-all-41.json +0 -310
  446. chronaxis/guide/en/fields/mapping/analysis-all-42.json +0 -346
  447. chronaxis/guide/en/fields/mapping/analysis-all-43.json +0 -267
  448. chronaxis/guide/en/fields/mapping/analysis-all-44.json +0 -327
  449. chronaxis/guide/en/fields/mapping/analysis-all-45.json +0 -279
  450. chronaxis/guide/en/fields/mapping/analysis-all-46.json +0 -327
  451. chronaxis/guide/en/fields/mapping/analysis-all-47.json +0 -312
  452. chronaxis/guide/en/fields/mapping/analysis-all-48.json +0 -294
  453. chronaxis/guide/en/fields/mapping/analysis-all-49.json +0 -253
  454. chronaxis/guide/en/fields/mapping/analysis-all-5.json +0 -312
  455. chronaxis/guide/en/fields/mapping/analysis-all-50.json +0 -346
  456. chronaxis/guide/en/fields/mapping/analysis-all-51.json +0 -324
  457. chronaxis/guide/en/fields/mapping/analysis-all-52.json +0 -335
  458. chronaxis/guide/en/fields/mapping/analysis-all-53.json +0 -317
  459. chronaxis/guide/en/fields/mapping/analysis-all-54.json +0 -316
  460. chronaxis/guide/en/fields/mapping/analysis-all-55.json +0 -302
  461. chronaxis/guide/en/fields/mapping/analysis-all-56.json +0 -319
  462. chronaxis/guide/en/fields/mapping/analysis-all-57.json +0 -334
  463. chronaxis/guide/en/fields/mapping/analysis-all-58.json +0 -333
  464. chronaxis/guide/en/fields/mapping/analysis-all-59.json +0 -359
  465. chronaxis/guide/en/fields/mapping/analysis-all-6.json +0 -327
  466. chronaxis/guide/en/fields/mapping/analysis-all-60.json +0 -303
  467. chronaxis/guide/en/fields/mapping/analysis-all-61.json +0 -326
  468. chronaxis/guide/en/fields/mapping/analysis-all-64.json +0 -367
  469. chronaxis/guide/en/fields/mapping/analysis-all-65.json +0 -334
  470. chronaxis/guide/en/fields/mapping/analysis-all-66.json +0 -358
  471. chronaxis/guide/en/fields/mapping/analysis-all-67.json +0 -349
  472. chronaxis/guide/en/fields/mapping/analysis-all-68.json +0 -331
  473. chronaxis/guide/en/fields/mapping/analysis-all-69.json +0 -304
  474. chronaxis/guide/en/fields/mapping/analysis-all-7.json +0 -303
  475. chronaxis/guide/en/fields/mapping/analysis-all-70.json +0 -349
  476. chronaxis/guide/en/fields/mapping/analysis-all-71.json +0 -288
  477. chronaxis/guide/en/fields/mapping/analysis-all-72.json +0 -316
  478. chronaxis/guide/en/fields/mapping/analysis-all-73.json +0 -266
  479. chronaxis/guide/en/fields/mapping/analysis-all-74.json +0 -227
  480. chronaxis/guide/en/fields/mapping/analysis-all-75.json +0 -239
  481. chronaxis/guide/en/fields/mapping/analysis-all-76.json +0 -274
  482. chronaxis/guide/en/fields/mapping/analysis-all-77.json +0 -380
  483. chronaxis/guide/en/fields/mapping/analysis-all-78.json +0 -313
  484. chronaxis/guide/en/fields/mapping/analysis-all-79.json +0 -338
  485. chronaxis/guide/en/fields/mapping/analysis-all-8.json +0 -292
  486. chronaxis/guide/en/fields/mapping/analysis-all-81.json +0 -140
  487. chronaxis/guide/en/fields/mapping/analysis-all-9.json +0 -332
  488. chronaxis/guide/en/fields/mapping/io_policy-all-1.json +0 -19
  489. chronaxis/guide/en/fields/mapping/mapping_version-all-1.json +0 -19
  490. chronaxis/guide/en/fields/mapping/path_base-all-1.json +0 -19
  491. chronaxis/guide/en/fields/mapping/sections-all-1.json +0 -402
  492. chronaxis/guide/en/fields/mapping/sections-all-2.json +0 -232
  493. chronaxis/guide/fields/mapping/aliases-all-1.json +0 -91
  494. chronaxis/guide/fields/mapping/analysis-all-1.json +0 -382
  495. chronaxis/guide/fields/mapping/analysis-all-10.json +0 -307
  496. chronaxis/guide/fields/mapping/analysis-all-11.json +0 -311
  497. chronaxis/guide/fields/mapping/analysis-all-12.json +0 -329
  498. chronaxis/guide/fields/mapping/analysis-all-13.json +0 -294
  499. chronaxis/guide/fields/mapping/analysis-all-14.json +0 -311
  500. chronaxis/guide/fields/mapping/analysis-all-15.json +0 -280
  501. chronaxis/guide/fields/mapping/analysis-all-16.json +0 -227
  502. chronaxis/guide/fields/mapping/analysis-all-17.json +0 -230
  503. chronaxis/guide/fields/mapping/analysis-all-18.json +0 -221
  504. chronaxis/guide/fields/mapping/analysis-all-19.json +0 -237
  505. chronaxis/guide/fields/mapping/analysis-all-2.json +0 -342
  506. chronaxis/guide/fields/mapping/analysis-all-20.json +0 -233
  507. chronaxis/guide/fields/mapping/analysis-all-21.json +0 -244
  508. chronaxis/guide/fields/mapping/analysis-all-22.json +0 -228
  509. chronaxis/guide/fields/mapping/analysis-all-23.json +0 -226
  510. chronaxis/guide/fields/mapping/analysis-all-24.json +0 -230
  511. chronaxis/guide/fields/mapping/analysis-all-25.json +0 -227
  512. chronaxis/guide/fields/mapping/analysis-all-26.json +0 -230
  513. chronaxis/guide/fields/mapping/analysis-all-27.json +0 -221
  514. chronaxis/guide/fields/mapping/analysis-all-28.json +0 -237
  515. chronaxis/guide/fields/mapping/analysis-all-29.json +0 -233
  516. chronaxis/guide/fields/mapping/analysis-all-3.json +0 -322
  517. chronaxis/guide/fields/mapping/analysis-all-30.json +0 -244
  518. chronaxis/guide/fields/mapping/analysis-all-31.json +0 -228
  519. chronaxis/guide/fields/mapping/analysis-all-32.json +0 -226
  520. chronaxis/guide/fields/mapping/analysis-all-33.json +0 -230
  521. chronaxis/guide/fields/mapping/analysis-all-34.json +0 -227
  522. chronaxis/guide/fields/mapping/analysis-all-35.json +0 -230
  523. chronaxis/guide/fields/mapping/analysis-all-36.json +0 -221
  524. chronaxis/guide/fields/mapping/analysis-all-37.json +0 -291
  525. chronaxis/guide/fields/mapping/analysis-all-38.json +0 -315
  526. chronaxis/guide/fields/mapping/analysis-all-39.json +0 -304
  527. chronaxis/guide/fields/mapping/analysis-all-4.json +0 -312
  528. chronaxis/guide/fields/mapping/analysis-all-40.json +0 -307
  529. chronaxis/guide/fields/mapping/analysis-all-41.json +0 -292
  530. chronaxis/guide/fields/mapping/analysis-all-42.json +0 -333
  531. chronaxis/guide/fields/mapping/analysis-all-43.json +0 -296
  532. chronaxis/guide/fields/mapping/analysis-all-44.json +0 -322
  533. chronaxis/guide/fields/mapping/analysis-all-45.json +0 -298
  534. chronaxis/guide/fields/mapping/analysis-all-46.json +0 -347
  535. chronaxis/guide/fields/mapping/analysis-all-47.json +0 -302
  536. chronaxis/guide/fields/mapping/analysis-all-48.json +0 -263
  537. chronaxis/guide/fields/mapping/analysis-all-49.json +0 -301
  538. chronaxis/guide/fields/mapping/analysis-all-5.json +0 -313
  539. chronaxis/guide/fields/mapping/analysis-all-50.json +0 -336
  540. chronaxis/guide/fields/mapping/analysis-all-51.json +0 -335
  541. chronaxis/guide/fields/mapping/analysis-all-52.json +0 -335
  542. chronaxis/guide/fields/mapping/analysis-all-53.json +0 -334
  543. chronaxis/guide/fields/mapping/analysis-all-54.json +0 -339
  544. chronaxis/guide/fields/mapping/analysis-all-55.json +0 -335
  545. chronaxis/guide/fields/mapping/analysis-all-56.json +0 -336
  546. chronaxis/guide/fields/mapping/analysis-all-57.json +0 -335
  547. chronaxis/guide/fields/mapping/analysis-all-58.json +0 -333
  548. chronaxis/guide/fields/mapping/analysis-all-59.json +0 -325
  549. chronaxis/guide/fields/mapping/analysis-all-6.json +0 -323
  550. chronaxis/guide/fields/mapping/analysis-all-60.json +0 -334
  551. chronaxis/guide/fields/mapping/analysis-all-61.json +0 -311
  552. chronaxis/guide/fields/mapping/analysis-all-63.json +0 -383
  553. chronaxis/guide/fields/mapping/analysis-all-64.json +0 -357
  554. chronaxis/guide/fields/mapping/analysis-all-65.json +0 -375
  555. chronaxis/guide/fields/mapping/analysis-all-66.json +0 -351
  556. chronaxis/guide/fields/mapping/analysis-all-67.json +0 -346
  557. chronaxis/guide/fields/mapping/analysis-all-68.json +0 -317
  558. chronaxis/guide/fields/mapping/analysis-all-69.json +0 -313
  559. chronaxis/guide/fields/mapping/analysis-all-7.json +0 -306
  560. chronaxis/guide/fields/mapping/analysis-all-70.json +0 -326
  561. chronaxis/guide/fields/mapping/analysis-all-71.json +0 -316
  562. chronaxis/guide/fields/mapping/analysis-all-72.json +0 -264
  563. chronaxis/guide/fields/mapping/analysis-all-73.json +0 -228
  564. chronaxis/guide/fields/mapping/analysis-all-74.json +0 -241
  565. chronaxis/guide/fields/mapping/analysis-all-75.json +0 -273
  566. chronaxis/guide/fields/mapping/analysis-all-76.json +0 -382
  567. chronaxis/guide/fields/mapping/analysis-all-77.json +0 -328
  568. chronaxis/guide/fields/mapping/analysis-all-78.json +0 -338
  569. chronaxis/guide/fields/mapping/analysis-all-8.json +0 -309
  570. chronaxis/guide/fields/mapping/analysis-all-80.json +0 -140
  571. chronaxis/guide/fields/mapping/analysis-all-9.json +0 -329
  572. chronaxis/guide/fields/mapping/io_policy-all-1.json +0 -19
  573. chronaxis/guide/fields/mapping/mapping_version-all-1.json +0 -19
  574. chronaxis/guide/fields/mapping/path_base-all-1.json +0 -19
  575. chronaxis/guide/fields/mapping/sections-all-1.json +0 -402
  576. chronaxis/guide/fields/mapping/sections-all-2.json +0 -232
  577. chronaxis/guide/schemas/legacy-import-v1.schema.json +0 -143
  578. chronaxis/migration/__init__.py +0 -11
  579. chronaxis/migration/api.py +0 -270
  580. chronaxis/migration/ini.py +0 -110
  581. chronaxis/migration/legacy_sum.py +0 -304
  582. chronaxis/migration/models.py +0 -83
  583. chronaxis/migration/publication.py +0 -230
  584. chronaxis/migration/sclicer.py +0 -241
  585. chronaxis/migration/slope.py +0 -78
  586. chronaxis/migration/start_end.py +0 -48
  587. {chronaxis-0.1.0.dev0.dist-info → chronaxis-0.1.0.dev2.dist-info}/WHEEL +0 -0
  588. {chronaxis-0.1.0.dev0.dist-info → chronaxis-0.1.0.dev2.dist-info}/entry_points.txt +0 -0
  589. {chronaxis-0.1.0.dev0.dist-info → chronaxis-0.1.0.dev2.dist-info}/licenses/LICENSE +0 -0
  590. {chronaxis-0.1.0.dev0.dist-info → chronaxis-0.1.0.dev2.dist-info}/top_level.txt +0 -0
chronaxis/__init__.py CHANGED
@@ -24,7 +24,7 @@ __all__ = [
24
24
  "parse_datetime_ns", "parse_duration", "shift_times_reference", "time_deltas_reference",
25
25
  "AnalysisPlan", "PlanValidationError", "load_analysis_plan", "validate_analysis_plan", "explain_analysis_plan",
26
26
  "PresetSpec", "PlanFragment", "build_preset", "resolve_preset_roles",
27
- "AnalysisResult", "OutputManifest", "RunMetadata", "AnalysisError", "ResultClosedError",
27
+ "AnalysisResult", "OutputManifest", "ExportManifest", "RunMetadata", "AnalysisError", "ResultClosedError",
28
28
  "AnalysisRunner",
29
29
  "BatchJob", "BatchJobResult", "BatchReport", "BatchRunner", "BatchPlan", "load_batch_plan", "run_batch",
30
30
  ]
@@ -52,10 +52,10 @@ def __getattr__(name: str):
52
52
  # 取得类本身不创建session或输入;validate/explain仍只消费冻结计划声明。
53
53
  from .runner import AnalysisRunner
54
54
  return AnalysisRunner
55
- if name in ('AnalysisResult', 'OutputManifest', 'RunMetadata', 'AnalysisError', 'ResultClosedError'):
55
+ if name in ('AnalysisResult', 'OutputManifest', 'ExportManifest', 'RunMetadata', 'AnalysisError', 'ResultClosedError'):
56
56
  # 结果层只依赖标准库和核心模型,访问入口也不启动任何读取或发布。
57
- from .results import AnalysisResult, OutputManifest, RunMetadata, AnalysisError, ResultClosedError
58
- return {'AnalysisResult': AnalysisResult, 'OutputManifest': OutputManifest, 'RunMetadata': RunMetadata,
57
+ from .results import AnalysisResult, OutputManifest, ExportManifest, RunMetadata, AnalysisError, ResultClosedError
58
+ return {'AnalysisResult': AnalysisResult, 'OutputManifest': OutputManifest, 'ExportManifest': ExportManifest, 'RunMetadata': RunMetadata,
59
59
  'AnalysisError': AnalysisError, 'ResultClosedError': ResultClosedError}[name]
60
60
  if name in ('PresetSpec', 'PlanFragment', 'build_preset', 'resolve_preset_roles'):
61
61
  # 预设是外部领域构建层,根包普通导入仍仅加载标准库时间协议。
chronaxis/__main__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """将python -m chronaxis委托给同一个CLI入口。
2
2
 
3
3
  输入为进程参数,输出及退出码由cli.main定义。作为模块导入时不运行命令;
4
- 实际执行只加载CLI,再由用户命令决定检查计划、运行发布或导入旧INI。
4
+ 实际执行只加载CLI,再由用户命令决定检查配置、运行分析或处理结果。
5
5
  本文件不另写计算逻辑、加载CAN或注册额外后台动作。
6
6
  """
7
7
 
@@ -0,0 +1,69 @@
1
+ """批量工作进程控制消息;只在收到对应任务时加载计算实现。"""
2
+ from dataclasses import replace
3
+ import os
4
+ from pathlib import Path
5
+ import signal
6
+ from threading import Event
7
+
8
+ def _diagnostics(error) -> tuple[str, ...]:
9
+ """保留有限异常链而不持有栈。参数: error原异常。返回: 文本tuple。异常: 原异常格式错误。"""
10
+ seen, details = set(), []
11
+ while error is not None and id(error) not in seen:
12
+ seen.add(id(error))
13
+ details.append(type(error).__name__ + ': ' + str(error))
14
+ details.extend(getattr(error, '__notes__', ()))
15
+ error = error.__cause__
16
+ return tuple(details)
17
+
18
+
19
+ def worker_main(connection, fast_cancel=None):
20
+ """处理run/cleanup/close命令。参数: 私有管道。返回: None。异常: 断管退出,父进程负责树清理。"""
21
+ if fast_cancel is None:
22
+ from .batch.runner import _run_job
23
+ from .batch.process_transport import restore_job
24
+ from .batch.models import BatchJobResult
25
+ # Ctrl+C只由调度主进程接收,正在写结果的工作进程继续完整收尾。
26
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
27
+ recovery = None
28
+ try:
29
+ while True:
30
+ command = connection.recv()
31
+ if command[0] == 'close':
32
+ if recovery is not None:
33
+ recovery.close()
34
+ return
35
+ if command[0] == 'cleanup':
36
+ try:
37
+ if recovery is not None:
38
+ recovery.close()
39
+ recovery = None
40
+ connection.send(('cleaned', None))
41
+ except BaseException as error:
42
+ connection.send(('cleanup_failed', _diagnostics(error)))
43
+ continue
44
+ if recovery is not None:
45
+ raise RuntimeError('存在未关闭资源的工作进程不能接收新任务')
46
+ _, packet, directory, backend = command
47
+ if fast_cancel is not None:
48
+ if command[0] != 'fast':
49
+ raise RuntimeError('fast 工作进程收到未知命令')
50
+ from .fast.worker import file_response
51
+ connection.send(file_response(packet, Path(directory), fast_cancel))
52
+ continue
53
+ try:
54
+ job = restore_job(packet)
55
+ outcome = _run_job(job, Path(directory), backend, Event(), Event())
56
+ except BaseException as error:
57
+ outcome = BatchJobResult(packet['job_id'], 'failed',
58
+ plan_path=packet.get('path'), error_type=type(error).__name__,
59
+ error_message=str(error), error_chain=_diagnostics(error))
60
+ recovery = outcome.recovery
61
+ pending = recovery is not None and not recovery.closed
62
+ # 只发送冻结的纯结果字段;CleanupHandle的原资源必须留在拥有它的进程。
63
+ response = ('result', replace(outcome, recovery=None, worker_pid=os.getpid()), pending)
64
+ connection.send(response)
65
+ except EOFError:
66
+ if recovery is not None:
67
+ recovery.close()
68
+ finally:
69
+ connection.close()
chronaxis/analysis_cli.py CHANGED
@@ -4,7 +4,7 @@
4
4
  validate和explain直接调用同一AnalysisRunner接口,不读取CSV、DBC或旧日志,不建立
5
5
  运行目录。序列化逐项选择公开报告字段,不遍历任意对象或另做一套配置校验。
6
6
  报告中的预算和有效期是声明,execution_ready不证明实际输入已核实。
7
- run先检查新输出与工作路径,再交给AnalysisRunner运行,在同一结果上write并关闭。
7
+ run先检查输出根与独占工作路径,再交给AnalysisRunner运行,在同一结果上write并关闭。
8
8
  只有真实关闭成功才打印成功摘要;失败保留原原因链和现存产物,不假报回滚。
9
9
 
10
10
  本模块仅在实际计划命令时加载;CLI帮助与参数错误仍只需要标准库。
@@ -95,7 +95,7 @@ def run_static(arguments) -> int:
95
95
 
96
96
 
97
97
  def _new_output(arguments) -> Path:
98
- """在扫描来源前拒绝已有输出、历史目录和必然冲突的工作路径。
98
+ """在扫描来源前检查可复用输出根、历史目录和冲突工作路径。
99
99
 
100
100
  参数: arguments包含output和可选work_dir,由argparse限定为路径文本。
101
101
  返回: 绝对输出Path;不创建目录,工作目录实际分配仍归AnalysisRunner。
@@ -115,12 +115,14 @@ def _new_output(arguments) -> Path:
115
115
  raise PlanValidationError('$.cli.work_dir', '工作目录不能等于输出目录或位于输出目录内部')
116
116
  for path in candidates:
117
117
  try:
118
- # 两个参数自身都必须全新,不能跟随已有的悬空工作链接创建其目标。
118
+ # 工作路径必须新建;输出允许已有目录,悬空链接不允许。
119
119
  # 最终输出安装的竞态仍由原write检查,运行会话也保留原工作目录检查。
120
120
  path.lstat()
121
121
  except FileNotFoundError:
122
122
  continue
123
- raise FileExistsError('输出或工作目标已存在,拒绝使用:' + str(path))
123
+ if path == output and path.is_dir() and not path.is_symlink():
124
+ continue
125
+ raise FileExistsError('工作目录须新建,输出目标须为目录:' + str(path))
124
126
  return output
125
127
 
126
128
 
@@ -143,15 +145,16 @@ def _cause_chain(error) -> tuple:
143
145
  def run_analysis(arguments) -> int:
144
146
  """复用真实分析和发布,成功关闭所有结果资源后才报告completed。
145
147
 
146
- 参数: arguments含plan、output、可选backend及work_dir;输出必须为全新目录。
148
+ 参数: arguments含plan、output、可选backend及work_dir;输出目录可复用。
147
149
  返回: 成功0;已知分析/发布/关闭失败1,并说明输出可能已经发布。
148
150
  异常: 配置/路径错误交主CLI分类;取消原样传播;MemoryError及已知编程错误
149
151
  的直接或AnalysisError原因链继续抛出,不伪装成普通业务错误。
150
- 副作用: AnalysisRunner建立专用暂存并读来源,write按原合同安装新目录;不删除产物。
152
+ 副作用: AnalysisRunner建立专用暂存并读来源,write在输出根新增本次文件;不删除已有产物。
151
153
  """
152
154
  plan = load_analysis_plan(arguments.plan)
153
155
  output = _new_output(arguments)
154
156
  runner = AnalysisRunner(backend=arguments.backend)
157
+ manifest = None
155
158
  try:
156
159
  # 结果拥有其真实仓和资源;write不重新运行来源,退出上下文完成实际close。
157
160
  with runner.run(plan, work_dir=arguments.work_dir) as result:
@@ -172,6 +175,11 @@ def run_analysis(arguments) -> int:
172
175
  if any(isinstance(item, (MemoryError, TypeError, AttributeError, AssertionError, NameError)) for item in chain):
173
176
  raise
174
177
  causes = ' <- '.join(type(item).__name__ + ': ' + str(item).replace('\n', ' ') for item in chain)
178
+ completed = manifest or next((getattr(item, 'completed_manifest') for item in chain
179
+ if getattr(item, 'completed_manifest', None) is not None), None)
180
+ if completed is not None:
181
+ print('结果已发布,清单:' + str(completed.manifest_path) + ';收尾失败:' + causes, file=sys.stderr)
182
+ return 1
175
183
  # 目录可能已安装后才发生close错误,非零退出不证明输出被撤销。
176
184
  print('分析或发布失败(输出可能已发布,请核对目标目录 ' + str(output) + '):' + causes,
177
185
  file=sys.stderr)
chronaxis/batch/api.py CHANGED
@@ -13,7 +13,7 @@ from .publication import write_batch_report
13
13
  def run_batch(plan: BatchPlan, output_dir, *, process_count: int | None = None, cancel_event=None, device_label='pending'):
14
14
  """运行并发布批量报告。
15
15
 
16
- 参数: plan为load_batch_plan取得的BatchPlan,规定本次独立任务及调度设置;process_count为可选正整数覆盖,None沿用计划;output_dir全新目录;cancel_event为可选Event;device_label为实际标签。
16
+ 参数: plan为load_batch_plan取得的BatchPlan,规定本次独立任务及调度设置;process_count为可选正整数覆盖,None沿用计划;output_dir可复用目录;cancel_event为可选Event;device_label为实际标签。
17
17
  返回: BatchReport;failed/cancelled保留在报告,不伪装全部成功。
18
18
  异常: TypeError为非BatchPlan或取消对象类型不符;ValueError为无效并发、后端或
19
19
  任务声明;目录创建、调度、报告发布错误按原类型传播。单任务失败记录在jobs中,
@@ -33,14 +33,21 @@ def run_batch(plan: BatchPlan, output_dir, *, process_count: int | None = None,
33
33
  report = replace(report, infrastructure_error=type(error).__name__ + ': ' + str(error))
34
34
  error.batch_report = report
35
35
  try:
36
- write_batch_report(report)
36
+ report = replace(report, report_path=write_batch_report(report))
37
+ error.batch_report = report
37
38
  except BaseException as diagnostic:
38
- error.add_note('批量失败报告未发布: ' + type(diagnostic).__name__)
39
+ if hasattr(diagnostic, 'published_report_path'):
40
+ report = replace(report, report_path=diagnostic.published_report_path)
41
+ error.batch_report = report
42
+ error.add_note(('批量报告已发布但收尾失败: ' if hasattr(diagnostic, 'published_report_path')
43
+ else '批量失败报告未发布: ') + type(diagnostic).__name__)
39
44
  raise
40
45
  try:
41
- write_batch_report(report)
46
+ report = replace(report, report_path=write_batch_report(report))
42
47
  except BaseException as error:
43
48
  # 已完成任务绝不因汇总文件失败而丢失,未清理资源仍可从原异常恢复。
49
+ if hasattr(error, 'published_report_path'):
50
+ report = replace(report, report_path=error.published_report_path)
44
51
  error.batch_report = report
45
52
  raise
46
53
  return report
chronaxis/batch/cli.py CHANGED
@@ -41,7 +41,7 @@ def run_batch_cli(arguments) -> int:
41
41
  print('批量结果清理失败:' + type(error).__name__ + ': ' + str(error), file=sys.stderr)
42
42
  document = report.to_dict()
43
43
  print(json.dumps(dict(status=report.status, counts=document['counts'],
44
- output=str(report.output_dir), report=str(report.output_dir/'batch_report.json'),
44
+ output=str(report.output_dir), report=str(report.report_path),
45
45
  cleanup_pending=cleanup_failed, device_label=label), ensure_ascii=False, allow_nan=False))
46
46
  if cleanup_failed or report.status == 'failed':
47
47
  return 1
chronaxis/batch/models.py CHANGED
@@ -84,6 +84,23 @@ class BatchPlan:
84
84
  返回: 冻结BatchPlan。异常: PlanValidationError定位字段;不读取业务报文,模板可读取DBC元数据。
85
85
  """
86
86
  from ..planning.checks import object_at, array_at, text_at, model_at, PlanValidationError, number_at, choice_at
87
+ from ..planning.diagnostic_collection import DiagnosticCollection
88
+ from ..planning.checks import require_json_value
89
+ require_json_value(config, '$')
90
+ diagnostics = DiagnosticCollection(config, 'batch-v1.schema.json')
91
+ names = set()
92
+ if type(config) is dict and type(config.get('jobs')) is list:
93
+ for index, job in enumerate(config['jobs']):
94
+ path = f'$.jobs[{index}]'
95
+ if type(job) is not dict or not diagnostics.usable(path):
96
+ continue
97
+ name = job.get('name', f'job_{index+1}').casefold()
98
+ if name in names:
99
+ diagnostics.add(PlanValidationError(path+'.name', '任务名称不能重复或仅大小写不同',
100
+ 'duplicate_id', hint='为每个独立任务指定唯一name'))
101
+ names.add(name)
102
+ # 批量自身结构一次报告;jobs子计划仍在工作进程启动时才加载。
103
+ diagnostics.finish()
87
104
  if type(config) is dict and ('workers' in config or 'max_workers' in config):
88
105
  raise PlanValidationError('$.process_count', '请使用process_count设置分析工作进程数;workers/max_workers不再接受')
89
106
  raw = object_at(config, '$', ('batch_version',), ('jobs', 'template', 'inputs', 'source', 'process_count', 'fail_fast', 'backend'))
@@ -129,6 +146,7 @@ class BatchJobResult:
129
146
  input_identity: str | None = None # 只有实际分析返回后才记录。
130
147
  run_id: str | None = None # 同计划重跑仍有独立运行身份。
131
148
  output_dir: Path | None = None # 实际发布成功的目录,不预填未生成输出。
149
+ manifest_path: Path | None = None # 本次任务实际完成清单,不猜固定文件名。
132
150
  error_type: str | None = None # 原异常类型或未启动取消原因。
133
151
  error_message: str | None = None # 原错误说明,不用成功值覆盖失败。
134
152
  error_chain: tuple[str, ...] = () # cause链和清理notes,保留错误来源。
@@ -151,6 +169,7 @@ class BatchJobResult:
151
169
  'elapsed_seconds', 'memory_budget_bytes', 'worker_pid')
152
170
  result = {name: getattr(self, name) for name in names}
153
171
  result.update(output_dir=None if self.output_dir is None else str(self.output_dir),
172
+ manifest_path=None if self.manifest_path is None else str(self.manifest_path),
154
173
  error_chain=list(self.error_chain),
155
174
  cleanup_pending=self.recovery is not None and not self.recovery.closed)
156
175
  return result
@@ -160,7 +179,7 @@ class BatchJobResult:
160
179
  class BatchReport:
161
180
  """按原任务顺序汇总结果;每个任务数值表仍在各自目录,不跨口径归约。"""
162
181
 
163
- output_dir: Path # 本批新目录,已有目标不会被覆盖。
182
+ output_dir: Path # 可复用的输出根,任务子目录和报告按本批时间区分。
164
183
  jobs: tuple[BatchJobResult, ...] # 最终结果顺序不受工作进程完成先后影响。
165
184
  process_count: int # 本次配置的工作进程上限。
166
185
  fail_fast: bool # 是否在观察到失败后停止派发。
@@ -168,6 +187,8 @@ class BatchReport:
168
187
  device_label: str # 调用方明确提供;缺省pending,不猜测机器。
169
188
  worker_process_count: int = 0 # 本次实际启动的工作进程数,空批次为0。
170
189
  infrastructure_error: str | None = None # 调度或汇总基础设施错误,与任务业务失败分开。
190
+ report_path: Path | None = None # 成功发布后返回的实际报告路径。
191
+ _publication: object = field(default=None, repr=False, compare=False) # 持有本批名称,关闭释放。
171
192
 
172
193
  @property
173
194
  def status(self) -> str:
@@ -190,6 +211,11 @@ class BatchReport:
190
211
  def close(self) -> None:
191
212
  """重试所有未完清理。参数: 无。返回: None。异常: 首个关闭错误,其他项仍尝试。"""
192
213
  first = None
214
+ if self._publication is not None:
215
+ try:
216
+ self._publication.close()
217
+ except BaseException as error:
218
+ first = error
193
219
  for job in self.jobs:
194
220
  if job.recovery is not None:
195
221
  try:
@@ -198,5 +224,7 @@ class BatchReport:
198
224
  # 一项失败不能阻止其他独立任务释放资源;原句柄仍由本报告保留。
199
225
  if first is None:
200
226
  first = error
227
+ else:
228
+ first.add_note('其他批量任务清理失败:' + str(error))
201
229
  if first is not None:
202
230
  raise first
@@ -6,15 +6,14 @@
6
6
 
7
7
  from dataclasses import replace
8
8
  import multiprocessing
9
- import os
10
9
  from pathlib import Path
11
- import signal
12
- from threading import Event
10
+ from time import monotonic
13
11
 
14
12
  from ..planning.plan import AnalysisPlan
15
13
  from ..planning.checks import json_value
16
14
  from .models import BatchJob, BatchJobResult, CleanupHandle
17
15
  from .process_tree import ProcessTree
16
+ from .._batch_worker import worker_main
18
17
 
19
18
 
20
19
  def job_packet(job):
@@ -35,60 +34,23 @@ def restore_job(packet):
35
34
  return BatchJob(packet['job_id'], plan)
36
35
 
37
36
 
38
- def worker_main(connection):
39
- """处理run/cleanup/close命令。参数: 私有管道。返回: None。异常: 断管退出,父进程负责树清理。"""
40
- from .runner import _run_job, _diagnostics
41
- # Ctrl+C只由调度主进程接收,正在写结果的工作进程继续完整收尾。
42
- signal.signal(signal.SIGINT, signal.SIG_IGN)
43
- recovery = None
44
- try:
45
- while True:
46
- command = connection.recv()
47
- if command[0] == 'close':
48
- if recovery is not None:
49
- recovery.close()
50
- return
51
- if command[0] == 'cleanup':
52
- try:
53
- if recovery is not None:
54
- recovery.close()
55
- recovery = None
56
- connection.send(('cleaned', None))
57
- except BaseException as error:
58
- connection.send(('cleanup_failed', _diagnostics(error)))
59
- continue
60
- if recovery is not None:
61
- raise RuntimeError('存在未关闭资源的工作进程不能接收新任务')
62
- _, packet, directory, backend = command
63
- try:
64
- job = restore_job(packet)
65
- outcome = _run_job(job, Path(directory), backend, Event(), Event())
66
- except BaseException as error:
67
- outcome = BatchJobResult(packet['job_id'], 'failed',
68
- plan_path=packet.get('path'), error_type=type(error).__name__,
69
- error_message=str(error), error_chain=_diagnostics(error))
70
- recovery = outcome.recovery
71
- pending = recovery is not None and not recovery.closed
72
- # 只发送冻结的纯结果字段;CleanupHandle的原资源必须留在拥有它的进程。
73
- connection.send(('result', replace(outcome, recovery=None, worker_pid=os.getpid()), pending))
74
- except EOFError:
75
- if recovery is not None:
76
- recovery.close()
77
- finally:
78
- connection.close()
79
-
80
-
81
37
  class ProcessWorker:
82
38
  """保存一个可复用工作进程、私有管道及其后代的清理责任。"""
83
39
 
84
- def __init__(self):
40
+ def __init__(self, *, fast=False):
85
41
  """启动等待命令的spawn进程。参数: 无。返回: 新对象。异常: 启动/树登记失败立即回收。"""
86
42
  context = multiprocessing.get_context('spawn')
87
43
  self.connection, child = context.Pipe()
88
- self.process = context.Process(target=worker_main, args=(child,), daemon=False)
44
+ self.fast_cancel = context.Event() if fast else None
45
+ self.cancel_deadline = None
46
+ self.fast_rows = None
47
+ args = (child, self.fast_cancel) if fast else (child,)
48
+ self.process = context.Process(target=worker_main, args=args, daemon=False)
89
49
  self.tree = None
90
50
  self.closed = False
91
51
  self.pending = False
52
+ self._pipe_lost = False
53
+ self._stop_requested = False
92
54
  try:
93
55
  self.process.start()
94
56
  self.tree = ProcessTree(self.process.pid)
@@ -107,21 +69,49 @@ class ProcessWorker:
107
69
  """发送独立任务。参数: job/目录/后端。返回: None。异常: 转换或通信错误。"""
108
70
  self.connection.send(('run', job_packet(job), str(directory), backend))
109
71
 
72
+ def submit_fast(self, job, directory, spec, total):
73
+ """只传准备完成的文件参数,工作进程不重新解析模板或建立下层进程。"""
74
+ packet = dict(job_id=job.job_id, spec=spec, total=total,
75
+ path=str(job.plan) if isinstance(job.plan, Path) else None)
76
+ self.connection.send(('fast', packet, str(directory), None))
77
+
78
+ def cancel_fast(self):
79
+ """通知在途文件,协作退出超过五秒才结束本次拥有的进程树。返回是否已强制回收。"""
80
+ if self.cancel_deadline is None:
81
+ self.fast_cancel.set()
82
+ self.cancel_deadline = monotonic() + 5
83
+ elif monotonic() >= self.cancel_deadline:
84
+ self._pipe_lost = True
85
+ self.close()
86
+ return True
87
+ return False
88
+
110
89
  def ready(self):
111
90
  """查询结果或异常退出。参数: 无。返回: bool。异常: 原管道错误。"""
112
91
  try:
113
92
  return self.connection.poll() or not self.process.is_alive()
114
93
  except (EOFError, OSError) as error:
94
+ if isinstance(error, (EOFError, BrokenPipeError)):
95
+ self._pipe_lost = True
115
96
  raise ChildProcessError('分析工作进程管道已断开,PID=' + str(self.pid)) from error
116
97
 
117
98
  def receive(self):
118
99
  """取结果并挂远程恢复句柄。参数: 无。返回: BatchJobResult。异常: 进程死亡或协议错误。"""
119
- if not self.connection.poll():
120
- raise ChildProcessError('分析工作进程异常退出,PID=' + str(self.pid))
121
100
  try:
122
- kind, result, self.pending = self.connection.recv()
123
- except EOFError as error:
101
+ # Windows 同伴退出时,探测或随后的读取都可能先报告断管。
102
+ if not self.connection.poll():
103
+ raise ChildProcessError('分析工作进程异常退出,PID=' + str(self.pid))
104
+ message = self.connection.recv()
105
+ except (EOFError, BrokenPipeError) as error:
106
+ self._pipe_lost = True
124
107
  raise ChildProcessError('分析工作进程断开,PID=' + str(self.pid)) from error
108
+ if len(message) == 4 and getattr(self,'fast_cancel',None) is not None:
109
+ kind, result, self.pending, self.fast_rows = message
110
+ else:
111
+ kind, result, self.pending = message
112
+ if kind == 'fast_result' and getattr(self,'fast_cancel',None) is not None and isinstance(result,dict):
113
+ result = BatchJobResult(**result)
114
+ kind = 'result'
125
115
  if kind != 'result' or not isinstance(result, BatchJobResult):
126
116
  raise RuntimeError('批量工作进程返回无效结果')
127
117
  return replace(result, recovery=CleanupHandle(self) if self.pending else None)
@@ -130,20 +120,36 @@ class ProcessWorker:
130
120
  """重试远程清理后关闭进程树。参数: 无。返回: None。异常: 清理失败保留全部所有者。"""
131
121
  if self.closed:
132
122
  return
133
- if self.process.is_alive():
134
- if self.pending:
135
- self.connection.send(('cleanup',))
136
- kind, detail = self.connection.recv()
137
- if kind != 'cleaned':
138
- raise RuntimeError('工作进程资源尚未关闭:' + '; '.join(detail))
139
- self.pending = False
140
- self.connection.send(('close',))
141
- self.process.join()
123
+ if not self._pipe_lost and not self._stop_requested and self.process.is_alive():
124
+ try:
125
+ if self.pending:
126
+ self.connection.send(('cleanup',))
127
+ kind, detail = self.connection.recv()
128
+ if kind != 'cleaned':
129
+ raise RuntimeError('工作进程资源尚未关闭:' + '; '.join(detail))
130
+ self.pending = False
131
+ self.connection.send(('close',))
132
+ self._stop_requested = True
133
+ except (EOFError, BrokenPipeError):
134
+ self._pipe_lost = True
135
+ if self._pipe_lost:
136
+ # 管道可先于进程退出;无法再握手时由父进程先结束本次拥有的树。
137
+ if not self._stop_requested:
138
+ if self.tree is not None:
139
+ self.tree.close()
140
+ self.tree = None
141
+ elif self.process.is_alive():
142
+ self.process.kill()
143
+ self._stop_requested = True
144
+ self.process.join(timeout=5)
145
+ if self.process.is_alive():
146
+ raise TimeoutError('等待断管工作进程退出超时,PID=' + str(self.pid))
142
147
  else:
143
148
  self.process.join()
144
- # 工作进程已退出后关闭作业,清除故障残余的CAN读取后代。
149
+ # 正常停止也需关闭作业,清除残余后代;前一步失败时保留所有者供重试。
145
150
  if self.tree is not None:
146
151
  self.tree.close()
152
+ self.tree = None
147
153
  self.connection.close()
148
154
  self.process.close()
149
- self.closed = True
155
+ self.pending, self.closed = False, True
@@ -7,16 +7,21 @@
7
7
  import json
8
8
  from pathlib import Path
9
9
  import platform
10
- from uuid import uuid4
10
+ import tempfile
11
11
  from .models import BatchReport
12
12
 
13
13
 
14
14
  def write_batch_report(report: BatchReport) -> Path:
15
- """发布固定汇总文件。参数: report实际结果。返回: batch_report.json路径。
15
+ """发布带本批时间的汇总文件。参数: report实际结果。返回: 实际报告路径。
16
16
 
17
17
  异常: 类型、只读路径、未支持平台、已有目标或原文件错误;关闭失败不安装正式文件。
18
18
  副作用: 只写本批目录的唯一暂存文件,失败不删除任务结果或覆盖已有报告。
19
19
  """
20
+ return _write_batch_report(report, {})
21
+
22
+
23
+ def _write_batch_report(report, extra):
24
+ """先完成可选的 fast 总量计算,再一次发布同一批报告。"""
20
25
  if not isinstance(report, BatchReport):
21
26
  raise TypeError('需要BatchReport')
22
27
  root = report.output_dir.absolute()
@@ -25,17 +30,32 @@ def write_batch_report(report: BatchReport) -> Path:
25
30
  raise ValueError('批量报告不能位于RefProjects')
26
31
  if platform.system() != 'Windows':
27
32
  raise NotImplementedError('批量报告原子发布目前只验收Windows')
28
- destination = root / 'batch_report.json'
29
- try:
30
- destination.lstat()
31
- except FileNotFoundError:
32
- pass
33
- else:
34
- raise FileExistsError('批量报告已存在,拒绝覆盖:' + str(destination))
35
- temporary = root / ('.batch-report-' + uuid4().hex + '.incomplete.json')
33
+ from ..results.publication import PublicationNames
34
+ publication = report._publication or PublicationNames(root, ('batch_report.json',))
35
+ destination = root / publication.names['batch_report.json']
36
+ from ..results.owned_directory import directory_identity, remove_owned_directory
37
+ temporary = identity = None
36
38
  # 编码、写入和真实close全部成功才安装;Windows rename在目标已存在时拒绝。
37
- with temporary.open('x', encoding='utf-8', newline='') as stream:
38
- json.dump(report.to_dict(), stream, ensure_ascii=False, allow_nan=False, indent=2)
39
- stream.write('\n')
40
- temporary.rename(destination)
41
- return destination
39
+ try:
40
+ temporary = Path(tempfile.mkdtemp(prefix='.batch-report-', suffix='.incomplete', dir=root))
41
+ identity = directory_identity(temporary)
42
+ with (temporary/'batch_report.json').open('x', encoding='utf-8', newline='') as stream:
43
+ json.dump(dict(report.to_dict(), **extra), stream, ensure_ascii=False, allow_nan=False, indent=2)
44
+ stream.write('\n')
45
+ publication.publish(temporary, 'batch_report.json', only=('batch_report.json',))
46
+ temporary.rmdir()
47
+ return destination
48
+ except BaseException as error:
49
+ if publication.completed:
50
+ error.published_report_path = destination
51
+ # 不撤销已完成任务;只清理本次报告临时目录,保留最初的写出错误。
52
+ try:
53
+ if temporary is not None and identity is not None:
54
+ remove_owned_directory(temporary, identity)
55
+ except BaseException as cleanup:
56
+ error.add_note('批量报告暂存清理失败:' + str(cleanup))
57
+ try:
58
+ publication.close()
59
+ except BaseException as cleanup:
60
+ error.add_note('批量命名锁释放失败:' + str(cleanup))
61
+ raise