nthlayer-workers 1.6.0__tar.gz → 2.0.0__tar.gz

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 (199) hide show
  1. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/PKG-INFO +4 -2
  2. nthlayer_workers-2.0.0/pyproject.toml +84 -0
  3. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/__init__.py +4 -0
  4. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/cli.py +3 -1
  5. nthlayer_workers-2.0.0/src/nthlayer_workers/cli_output.py +26 -0
  6. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/cli.py +11 -10
  7. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/correlation/changes.py +2 -2
  8. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/correlation/engine.py +2 -2
  9. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/correlation/temporal.py +2 -2
  10. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/ingestion/protocol.py +5 -2
  11. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/ingestion/severity.py +1 -0
  12. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/ingestion/webhook.py +9 -12
  13. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/prometheus.py +6 -4
  14. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/session.py +7 -7
  15. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/snapshot/generator.py +6 -3
  16. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/snapshot/model.py +4 -3
  17. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/state.py +3 -3
  18. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/traces/tempo.py +2 -2
  19. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/types.py +3 -3
  20. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/verdict_chain.py +1 -3
  21. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/worker.py +7 -8
  22. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/learn/__main__.py +2 -1
  23. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_apply.py +274 -0
  24. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_gh.py +163 -0
  25. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_interactive.py +162 -0
  26. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_interactive_app.py +229 -0
  27. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_preview.py +86 -0
  28. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_trigger.py +40 -0
  29. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/_yaml.py +286 -0
  30. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/cli.py +543 -0
  31. nthlayer_workers-2.0.0/src/nthlayer_workers/learn/recommendations.py +597 -0
  32. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/learn/retrospective.py +166 -44
  33. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/learn/worker.py +84 -20
  34. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/__main__.py +2 -1
  35. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/adapters/devin.py +1 -1
  36. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/adapters/gastown.py +1 -1
  37. nthlayer_workers-2.0.0/src/nthlayer_workers/measure/adapters/prometheus.py +483 -0
  38. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/adapters/webhook.py +6 -9
  39. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/calibration/loop.py +2 -2
  40. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/calibration/slos.py +2 -2
  41. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/calibration/verdict_calibration.py +2 -2
  42. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/cli.py +51 -203
  43. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/config.py +19 -11
  44. nthlayer_workers-2.0.0/src/nthlayer_workers/measure/governance/engine.py +31 -0
  45. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/pipeline/router.py +4 -3
  46. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/trends/tracker.py +2 -2
  47. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/types.py +6 -6
  48. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/worker.py +4 -5
  49. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/__main__.py +2 -1
  50. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/assessment.py +3 -3
  51. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/cli.py +16 -3
  52. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/decision_records.py +3 -0
  53. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/consul.py +2 -2
  54. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/etcd.py +3 -6
  55. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/kubernetes.py +3 -5
  56. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/zookeeper.py +1 -0
  57. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/discovery/__init__.py +6 -1
  58. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/discovery/client.py +6 -1
  59. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/discovery/models.py +3 -3
  60. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/drift/analyzer.py +9 -14
  61. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/explanation.py +1 -0
  62. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/gate/evaluator.py +14 -19
  63. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/gate/policies.py +2 -1
  64. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/incident.py +2 -2
  65. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/slo/collector.py +0 -1
  66. nthlayer_workers-2.0.0/src/nthlayer_workers/observe/slo/spec_loader.py +91 -0
  67. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/sqlite_store.py +2 -2
  68. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/verification/extractor.py +5 -1
  69. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/worker.py +1 -2
  70. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/__main__.py +2 -1
  71. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/agents/base.py +7 -5
  72. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/agents/communication.py +3 -2
  73. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/agents/investigation.py +1 -0
  74. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/agents/remediation.py +2 -1
  75. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/cli.py +21 -16
  76. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/coordinator.py +15 -13
  77. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/metrics.py +2 -2
  78. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/notification_backends/ntfy_backend.py +4 -4
  79. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/notification_backends/slack_backend.py +5 -5
  80. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/notification_backends/stdout_backend.py +2 -2
  81. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/oncall/escalation.py +0 -2
  82. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/oncall/runner.py +6 -6
  83. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/safe_actions/registry.py +5 -4
  84. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/server.py +3 -3
  85. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/sre/brief.py +2 -3
  86. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/sre/delegation.py +2 -2
  87. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/sre/suppression.py +2 -2
  88. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/types.py +3 -3
  89. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/verdict_submission.py +0 -1
  90. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/worker.py +5 -6
  91. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/worker_helpers.py +4 -4
  92. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/runner.py +0 -1
  93. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers.egg-info/PKG-INFO +4 -2
  94. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers.egg-info/SOURCES.txt +8 -5
  95. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers.egg-info/entry_points.txt +1 -0
  96. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers.egg-info/requires.txt +3 -1
  97. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/tests/test_cli_gate.py +0 -1
  98. nthlayer_workers-1.6.0/pyproject.toml +0 -47
  99. nthlayer_workers-1.6.0/src/nthlayer_workers/learn/cli.py +0 -164
  100. nthlayer_workers-1.6.0/src/nthlayer_workers/learn/recommendations.py +0 -313
  101. nthlayer_workers-1.6.0/src/nthlayer_workers/measure/adapters/prometheus.py +0 -277
  102. nthlayer_workers-1.6.0/src/nthlayer_workers/measure/api/normalise.py +0 -50
  103. nthlayer_workers-1.6.0/src/nthlayer_workers/measure/api/queue.py +0 -243
  104. nthlayer_workers-1.6.0/src/nthlayer_workers/measure/api/response.py +0 -51
  105. nthlayer_workers-1.6.0/src/nthlayer_workers/measure/api/server.py +0 -504
  106. nthlayer_workers-1.6.0/src/nthlayer_workers/measure/governance/engine.py +0 -163
  107. nthlayer_workers-1.6.0/src/nthlayer_workers/observe/slo/spec_loader.py +0 -40
  108. nthlayer_workers-1.6.0/src/nthlayer_workers/respond/safe_actions/__init__.py +0 -0
  109. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/README.md +0 -0
  110. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/setup.cfg +0 -0
  111. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/__init__.py +0 -0
  112. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/config.py +0 -0
  113. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/correlation/__init__.py +0 -0
  114. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/correlation/dedup.py +0 -0
  115. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/correlation/topology.py +0 -0
  116. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/ingestion/__init__.py +0 -0
  117. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/notifications.py +0 -0
  118. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/reasoning.py +1 -1
  119. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/snapshot/__init__.py +0 -0
  120. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/snapshot/token.py +0 -0
  121. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/store/__init__.py +0 -0
  122. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/store/protocol.py +0 -0
  123. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/store/sqlite.py +0 -0
  124. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/summary.py +1 -1
  125. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/traces/__init__.py +0 -0
  126. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/traces/protocol.py +0 -0
  127. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/correlate/traces/topology.py +0 -0
  128. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/learn/__init__.py +0 -0
  129. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/learn/trends.py +0 -0
  130. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/__init__.py +0 -0
  131. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/_parsing.py +0 -0
  132. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/adapters/__init__.py +0 -0
  133. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/adapters/_util.py +0 -0
  134. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/adapters/protocol.py +0 -0
  135. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/api → nthlayer_workers-2.0.0/src/nthlayer_workers/measure/calibration}/__init__.py +0 -0
  136. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/detection/__init__.py +0 -0
  137. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/detection/detector.py +0 -0
  138. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/detection/protocol.py +0 -0
  139. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/calibration → nthlayer_workers-2.0.0/src/nthlayer_workers/measure/governance}/__init__.py +0 -0
  140. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/manifest.py +0 -0
  141. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/notifications.py +0 -0
  142. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/governance → nthlayer_workers-2.0.0/src/nthlayer_workers/measure/pipeline}/__init__.py +0 -0
  143. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/pipeline/evaluator.py +2 -2
  144. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/pipeline → nthlayer_workers-2.0.0/src/nthlayer_workers/measure/store}/__init__.py +0 -0
  145. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/store/protocol.py +0 -0
  146. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/store/sqlite.py +0 -0
  147. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/telemetry.py +0 -0
  148. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/store → nthlayer_workers-2.0.0/src/nthlayer_workers/measure/tiering}/__init__.py +0 -0
  149. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/tiering/classifier.py +0 -0
  150. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/measure/tiering/promotion.py +0 -0
  151. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/tiering → nthlayer_workers-2.0.0/src/nthlayer_workers/measure/trends}/__init__.py +0 -0
  152. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/__init__.py +0 -0
  153. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/api/__init__.py +0 -0
  154. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/config.py +0 -0
  155. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/db/__init__.py +0 -0
  156. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/__init__.py +1 -1
  157. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/discovery.py +0 -0
  158. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/__init__.py +0 -0
  159. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/backstage.py +1 -1
  160. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/base.py +0 -0
  161. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/dependencies/providers/prometheus.py +2 -2
  162. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/deployments/__init__.py +0 -0
  163. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/discovery/classifier.py +0 -0
  164. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/drift/__init__.py +0 -0
  165. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/drift/models.py +0 -0
  166. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/drift/patterns.py +0 -0
  167. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/gate/__init__.py +0 -0
  168. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/gate/conditions.py +0 -0
  169. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/gate/correlator.py +0 -0
  170. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/gate_adapter.py +0 -0
  171. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/portfolio/__init__.py +1 -1
  172. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/portfolio/aggregator.py +0 -0
  173. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/portfolio/scorer.py +0 -0
  174. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/slo/__init__.py +1 -1
  175. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/store.py +0 -0
  176. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/verification/__init__.py +0 -0
  177. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/verification/exporter_guidance.py +0 -0
  178. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/verification/models.py +0 -0
  179. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/observe/verification/verifier.py +0 -0
  180. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/__init__.py +0 -0
  181. {nthlayer_workers-1.6.0/src/nthlayer_workers/measure/trends → nthlayer_workers-2.0.0/src/nthlayer_workers/respond/agents}/__init__.py +0 -0
  182. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/agents/response_models.py +0 -0
  183. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/agents/triage.py +0 -0
  184. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/config.py +0 -0
  185. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/context_store.py +1 -1
  186. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/notification_backends/__init__.py +0 -0
  187. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/notification_backends/protocol.py +0 -0
  188. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/notifications.py +1 -1
  189. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/oncall/__init__.py +0 -0
  190. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/oncall/schedule.py +0 -0
  191. {nthlayer_workers-1.6.0/src/nthlayer_workers/respond/agents → nthlayer_workers-2.0.0/src/nthlayer_workers/respond/safe_actions}/__init__.py +0 -0
  192. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/safe_actions/actions.py +0 -0
  193. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/safe_actions/webhook.py +0 -0
  194. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/sre/__init__.py +0 -0
  195. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/sre/post_incident.py +0 -0
  196. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers/respond/sre/shift_report.py +0 -0
  197. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers.egg-info/dependency_links.txt +0 -0
  198. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/src/nthlayer_workers.egg-info/top_level.txt +0 -0
  199. {nthlayer_workers-1.6.0 → nthlayer_workers-2.0.0}/tests/test_runner.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nthlayer-workers
3
- Version: 1.6.0
3
+ Version: 2.0.0
4
4
  Summary: NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: nthlayer-common<2.0.0,>=1.5.0
7
+ Requires-Dist: nthlayer-common<3.0.0,>=2.1.2
8
8
  Requires-Dist: httpx>=0.27
9
9
  Requires-Dist: pyyaml>=6.0
10
10
  Requires-Dist: structlog>=24.1.0
@@ -13,6 +13,8 @@ Requires-Dist: numpy>=1.24
13
13
  Requires-Dist: starlette>=0.40
14
14
  Requires-Dist: uvicorn>=0.30
15
15
  Requires-Dist: python-multipart>=0.0.18
16
+ Requires-Dist: ruamel.yaml>=0.18
17
+ Requires-Dist: textual>=1.0
16
18
  Provides-Extra: dev
17
19
  Requires-Dist: pytest>=8.2; extra == "dev"
18
20
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
@@ -0,0 +1,84 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nthlayer-workers"
7
+ version = "2.0.0"
8
+ description = "NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ # >=2.1.2, NOT >=2.1.0 or 2.1.1. 2.1.0 introduced foreign_yaml_reason with an
13
+ # INERT body-recovery step: it tested spec.slos as a list, but v1 writes
14
+ # a mapping and v2 has no spec.slos at all, so a header-stripped manifest
15
+ # was dropped with parse_failures == 0 — the exact silent drop this
16
+ # package uses the gate to prevent. Fixed across 2.1.1 and 2.1.2;
17
+ # 2.1.1 still counted every Backstage catalog-info.yaml as a broken
18
+ # manifest, which 2.1.2 corrected.
19
+ #
20
+ # CI resolves nthlayer-common from a sibling checkout, so it would not
21
+ # notice a bad resolve. tests/observe/test_slo_spec_loader.py's
22
+ # TestSharedGateResolvesCorrectly exists for that: it uses the shapes the
23
+ # PARSERS produce and fails against 2.1.0.
24
+ "nthlayer-common>=2.1.2,<3.0.0",
25
+ "httpx>=0.27",
26
+ "pyyaml>=6.0",
27
+ "structlog>=24.1.0",
28
+ "scipy>=1.11",
29
+ "numpy>=1.24",
30
+ "starlette>=0.40",
31
+ "uvicorn>=0.30",
32
+ # Required by ApprovalServer's Slack interaction endpoint at
33
+ # `respond/server.py:197` — `await request.form()` hard-imports
34
+ # python-multipart via Starlette. Operators running with
35
+ # `slack_signing_secret` set need this at runtime, not just for
36
+ # tests; declared as a top-level production dep.
37
+ "python-multipart>=0.0.18",
38
+ "ruamel.yaml>=0.18",
39
+ "textual>=1.0",
40
+ ]
41
+
42
+ [project.scripts]
43
+ nthlayer-workers = "nthlayer_workers.cli:main"
44
+ nthlayer-learn = "nthlayer_workers.learn.cli:main"
45
+
46
+ [project.optional-dependencies]
47
+ dev = [
48
+ "pytest>=8.2",
49
+ "pytest-asyncio>=0.23",
50
+ "respx>=0.21",
51
+ "ruff>=0.8",
52
+ ]
53
+
54
+ [tool.uv.sources]
55
+ nthlayer-common = { path = "../nthlayer-common", editable = true }
56
+
57
+ [tool.setuptools.packages.find]
58
+ where = ["src"]
59
+
60
+ [tool.pytest.ini_options]
61
+ asyncio_mode = "auto"
62
+ testpaths = ["tests"]
63
+
64
+ # Ruff configuration (opensrm-d96q floor). Explicit defaults so future
65
+ # violations show up as new findings rather than blending in. Tightening
66
+ # beyond E/F (isort/pyupgrade/simplify/bugbear) is tracked in
67
+ # opensrm-d96q.1 .. d96q.4 — each rule family is its own bead because
68
+ # the cumulative diff across all four is ~640 findings.
69
+ [tool.ruff]
70
+ target-version = "py311"
71
+ line-length = 100
72
+
73
+ [tool.ruff.lint]
74
+ # Mirrors ruff's literal default select (E4/E7/E9 + F) plus I (isort,
75
+ # opensrm-ncy0), UP (pyupgrade, opensrm-6nn1), SIM (simplify,
76
+ # opensrm-y7dd), and B (bugbear, opensrm-po23). E501 / W are separate
77
+ # hygiene calls (line-too-long has 393 hits today).
78
+ select = ["E4", "E7", "E9", "F", "I", "UP", "SIM", "B"]
79
+
80
+ [tool.ruff.lint.per-file-ignores]
81
+ # Correlate test files defer some imports until after pytest.importorskip
82
+ # / pytestmark blocks (the modules under test optional-import heavy deps).
83
+ # Pin E402 here instead of scattering noqa: E402 across the call sites.
84
+ "tests/correlate/test_*.py" = ["E402"]
@@ -3,3 +3,7 @@
3
3
  Internal modules: observe, measure, correlate, respond, learn.
4
4
  Communicates with nthlayer-core exclusively via HTTP API.
5
5
  """
6
+
7
+ from importlib.metadata import version
8
+
9
+ __version__ = version("nthlayer-workers")
@@ -4,12 +4,13 @@ import argparse
4
4
  import asyncio
5
5
  import sys
6
6
 
7
+ from nthlayer_workers import __version__
7
8
  from nthlayer_workers.runner import ModuleRunner
8
9
 
9
10
 
10
11
  def main():
11
12
  parser = argparse.ArgumentParser(description="NthLayer workers")
12
- parser.add_argument("-V", "--version", action="version", version="%(prog)s 1.5.0")
13
+ parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
13
14
  sub = parser.add_subparsers(dest="command")
14
15
 
15
16
  # -- serve: start all worker modules --
@@ -60,6 +61,7 @@ def main():
60
61
 
61
62
  if args.command == "serve":
62
63
  from nthlayer_common.api_client import CoreAPIClient
64
+
63
65
  from nthlayer_workers.observe.worker import (
64
66
  ObserveCollectModule,
65
67
  ObserveDriftModule,
@@ -0,0 +1,26 @@
1
+ """Operator-facing CLI output shared across worker modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def warn_parse_failures(parse_failures: int, specs_dir: str | Path) -> None:
10
+ """Tell the operator when the SLOs that follow cover only part of a directory.
11
+
12
+ Surfaced on stderr rather than only logged: whoever reads SLO output is
13
+ the one who needs to know the view is partial, and a warning in a log
14
+ they are not tailing does not reach them (opensrm-3470).
15
+
16
+ Takes the count, not a ``LoadedSpecs`` — observe and measure each have
17
+ their own, carrying differently-shaped SLOs under differently-named
18
+ fields (``service_slos`` vs ``slos``). The only thing they share is this
19
+ number, so that is the whole parameter (opensrm-fxln).
20
+ """
21
+ if parse_failures:
22
+ print(
23
+ f"Warning: {parse_failures} manifest(s) in {specs_dir} "
24
+ f"failed to parse and were not evaluated",
25
+ file=sys.stderr,
26
+ )
@@ -9,7 +9,7 @@ import signal
9
9
  import sys
10
10
  import tempfile
11
11
  from dataclasses import replace
12
- from datetime import datetime, timedelta, timezone
12
+ from datetime import UTC, datetime, timedelta
13
13
  from typing import Any
14
14
 
15
15
  import structlog
@@ -30,7 +30,7 @@ from nthlayer_workers.correlate.types import AgentState, EventType, SitRepEvent
30
30
 
31
31
  logger = structlog.get_logger()
32
32
 
33
- REFERENCE_TIME = datetime(2026, 1, 1, tzinfo=timezone.utc)
33
+ REFERENCE_TIME = datetime(2026, 1, 1, tzinfo=UTC)
34
34
 
35
35
  # Confidence decay window for temporal proximity heuristic (30 minutes)
36
36
  _PROXIMITY_WINDOW_SECONDS = 1800.0
@@ -245,10 +245,7 @@ def status_command(config_path: str | None, store_dir: str | None = None) -> int
245
245
  config = load_config(config_path) if config_path else SitRepConfig()
246
246
 
247
247
  # Use store_dir if provided (for testing), otherwise use config path
248
- if store_dir:
249
- db_path = os.path.join(store_dir, "sitrep-events.db")
250
- else:
251
- db_path = config.store_path
248
+ db_path = os.path.join(store_dir, "sitrep-events.db") if store_dir else config.store_path
252
249
 
253
250
  store = SQLiteEventStore(db_path)
254
251
  try:
@@ -332,7 +329,7 @@ async def _serve_loop(config: SitRepConfig) -> None:
332
329
  try:
333
330
  await asyncio.wait_for(shutdown.wait(), timeout=interval)
334
331
  break # shutdown requested
335
- except asyncio.TimeoutError:
332
+ except TimeoutError:
336
333
  pass # normal cycle
337
334
 
338
335
  # Drain buffered events into store (single-threaded, safe)
@@ -481,7 +478,7 @@ def correlate_command(
481
478
  if trace_backend is not None:
482
479
  try:
483
480
  baseline_td = _parse_duration(trace_baseline_window)
484
- end = datetime.now(tz=timezone.utc)
481
+ end = datetime.now(tz=UTC)
485
482
  start = end - timedelta(minutes=30)
486
483
  trace_evidence = await trace_backend.get_trace_evidence(
487
484
  services=sorted(affected),
@@ -540,7 +537,10 @@ def correlate_command(
540
537
  reasoning_mode = "heuristic"
541
538
 
542
539
  if reasoning:
543
- from nthlayer_workers.correlate.reasoning import reason_about_correlations, reasoning_available
540
+ from nthlayer_workers.correlate.reasoning import (
541
+ reason_about_correlations,
542
+ reasoning_available,
543
+ )
544
544
 
545
545
  if reasoning_available():
546
546
  kwargs = {}
@@ -629,6 +629,7 @@ def correlate_command(
629
629
  # canonical primitive). correlation_snapshot is in ASSESSMENT_KINDS;
630
630
  # producing a "correlation"-typed verdict here was a category error.
631
631
  import uuid as _uuid
632
+
632
633
  from nthlayer_workers.observe.assessment import Assessment as _Assessment
633
634
  from nthlayer_workers.observe.sqlite_store import SQLiteAssessmentStore
634
635
 
@@ -656,7 +657,7 @@ def correlate_command(
656
657
  }
657
658
  corr_assessment = _Assessment(
658
659
  id=corr_assessment_id,
659
- created_at=datetime.now(timezone.utc),
660
+ created_at=datetime.now(UTC),
660
661
  kind="correlation_snapshot",
661
662
  service=trigger_service,
662
663
  producer="nthlayer-correlate",
@@ -1,7 +1,7 @@
1
1
  """Change candidate indexing. Deterministic transport."""
2
2
  from __future__ import annotations
3
3
 
4
- from datetime import datetime, timezone
4
+ from datetime import UTC, datetime
5
5
 
6
6
  from nthlayer_workers.correlate.store.protocol import EventStore
7
7
  from nthlayer_workers.correlate.types import ChangeCandidate, TemporalGroup
@@ -11,7 +11,7 @@ def _parse_ts(ts: str) -> datetime:
11
11
  """Parse ISO 8601 timestamp. Always returns timezone-aware (UTC default)."""
12
12
  dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
13
13
  if dt.tzinfo is None:
14
- dt = dt.replace(tzinfo=timezone.utc)
14
+ dt = dt.replace(tzinfo=UTC)
15
15
  return dt
16
16
 
17
17
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import uuid
5
5
  from dataclasses import replace
6
- from datetime import datetime, timedelta, timezone
6
+ from datetime import UTC, datetime, timedelta
7
7
 
8
8
  from nthlayer_workers.correlate.correlation.changes import find_change_candidates
9
9
  from nthlayer_workers.correlate.correlation.dedup import deduplicate
@@ -36,7 +36,7 @@ class CorrelationEngine:
36
36
  7. Assemble CorrelationGroups with priority scoring
37
37
  """
38
38
  # Step 1: Query store for events in window
39
- now = datetime.now(timezone.utc)
39
+ now = datetime.now(UTC)
40
40
  start = (now - timedelta(minutes=window_minutes)).isoformat()
41
41
  end = now.isoformat()
42
42
 
@@ -1,7 +1,7 @@
1
1
  """Temporal grouping — windowed aggregation by service. Deterministic transport."""
2
2
  from __future__ import annotations
3
3
 
4
- from datetime import datetime, timezone
4
+ from datetime import UTC, datetime
5
5
 
6
6
  from nthlayer_workers.correlate.types import SitRepEvent, TemporalGroup
7
7
 
@@ -10,7 +10,7 @@ def _parse_ts(ts: str) -> datetime:
10
10
  """Parse ISO 8601 timestamp. Always returns timezone-aware (UTC default)."""
11
11
  dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
12
12
  if dt.tzinfo is None:
13
- dt = dt.replace(tzinfo=timezone.utc)
13
+ dt = dt.replace(tzinfo=UTC)
14
14
  return dt
15
15
 
16
16
 
@@ -1,10 +1,13 @@
1
1
  """Ingester protocol — the ingestion contract for SitRep."""
2
2
  from __future__ import annotations
3
- from typing import Awaitable, Callable, Protocol, Union
3
+
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Protocol
6
+
4
7
  from nthlayer_workers.correlate.types import SitRepEvent
5
8
 
6
9
 
7
10
  class Ingester(Protocol):
8
11
  async def start(self) -> None: ...
9
12
  async def stop(self) -> None: ...
10
- def on_event(self, handler: Callable[[SitRepEvent], Union[Awaitable[None], None]]) -> None: ...
13
+ def on_event(self, handler: Callable[[SitRepEvent], Awaitable[None] | None]) -> None: ...
@@ -1,5 +1,6 @@
1
1
  """Severity pre-scoring from SLO targets. Pure arithmetic, no judgment."""
2
2
  from __future__ import annotations
3
+
3
4
  from nthlayer_workers.correlate.types import SitRepEvent
4
5
 
5
6
 
@@ -2,11 +2,12 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
+ import contextlib
5
6
  import json
6
7
  import uuid
8
+ from collections.abc import Awaitable, Callable
7
9
  from dataclasses import replace
8
- from datetime import datetime, timezone
9
- from typing import Callable, Awaitable
10
+ from datetime import UTC, datetime
10
11
 
11
12
  from nthlayer_workers.correlate.ingestion import severity as _severity
12
13
  from nthlayer_workers.correlate.types import EventType, SitRepEvent
@@ -65,12 +66,12 @@ class WebhookIngester:
65
66
  raw_type = data["type"]
66
67
  try:
67
68
  event_type = EventType(raw_type)
68
- except ValueError:
69
- raise ValueError(f"Unknown event type: {raw_type!r}")
69
+ except ValueError as exc:
70
+ raise ValueError(f"Unknown event type: {raw_type!r}") from exc
70
71
 
71
72
  # Auto-generate id and timestamp when absent
72
73
  event_id = data.get("id") or str(uuid.uuid4())
73
- timestamp = data.get("timestamp") or datetime.now(timezone.utc).isoformat()
74
+ timestamp = data.get("timestamp") or datetime.now(UTC).isoformat()
74
75
 
75
76
  event = SitRepEvent(
76
77
  id=event_id,
@@ -101,12 +102,10 @@ class WebhookIngester:
101
102
  await asyncio.wait_for(
102
103
  self._handle_request(reader, writer), timeout=_CONNECTION_TIMEOUT
103
104
  )
104
- except asyncio.TimeoutError:
105
+ except TimeoutError:
105
106
  writer.close()
106
- try:
107
+ with contextlib.suppress(Exception):
107
108
  await writer.wait_closed()
108
- except Exception:
109
- pass
110
109
 
111
110
  async def _handle_request(
112
111
  self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
@@ -191,7 +190,5 @@ class WebhookIngester:
191
190
 
192
191
  finally:
193
192
  writer.close()
194
- try:
193
+ with contextlib.suppress(Exception):
195
194
  await writer.wait_closed()
196
- except Exception:
197
- pass
@@ -2,7 +2,8 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import uuid
5
- from datetime import datetime, timezone
5
+ from datetime import UTC, datetime
6
+
6
7
  import httpx
7
8
  import structlog
8
9
 
@@ -31,7 +32,7 @@ async def fetch_alerts(
31
32
  continue
32
33
  events.append(SitRepEvent(
33
34
  id=f"prom-alert-{uuid.uuid4().hex[:8]}",
34
- timestamp=alert.get("activeAt", datetime.now(timezone.utc).isoformat()),
35
+ timestamp=alert.get("activeAt", datetime.now(UTC).isoformat()),
35
36
  source="prometheus",
36
37
  type=EventType.ALERT,
37
38
  service=service,
@@ -56,7 +57,7 @@ async def fetch_metric_breaches(
56
57
  ) -> list[SitRepEvent]:
57
58
  """Query Prometheus for SLO metric breaches on given services."""
58
59
  events: list[SitRepEvent] = []
59
- now = datetime.now(timezone.utc)
60
+ now = datetime.now(UTC)
60
61
 
61
62
  for service in services:
62
63
  # Check error budget
@@ -152,9 +153,10 @@ def load_dependency_graph(specs_dir: str) -> dict[str, dict]:
152
153
 
153
154
  Returns dict mapping service name → {tier, dependencies, dependents}.
154
155
  """
155
- import yaml
156
156
  from pathlib import Path
157
157
 
158
+ import yaml
159
+
158
160
  graph: dict[str, dict] = {}
159
161
  specs_path = Path(specs_dir)
160
162
  if not specs_path.is_dir():
@@ -13,7 +13,7 @@ are what make correlation novel vs. simple alert aggregation.
13
13
  from __future__ import annotations
14
14
 
15
15
  from dataclasses import dataclass, field
16
- from datetime import datetime, timezone
16
+ from datetime import UTC, datetime
17
17
 
18
18
  from nthlayer_workers.correlate.types import SitRepEvent
19
19
 
@@ -39,8 +39,8 @@ class SessionWindow:
39
39
 
40
40
  domain: CorrelationDomain
41
41
  events: list[SitRepEvent] = field(default_factory=list)
42
- opened_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
43
- last_event_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
42
+ opened_at: datetime = field(default_factory=lambda: datetime.now(UTC))
43
+ last_event_at: datetime = field(default_factory=lambda: datetime.now(UTC))
44
44
  has_trigger: bool = False
45
45
 
46
46
  def _close_flags(
@@ -119,7 +119,7 @@ class SessionWindowManager:
119
119
  # Cap opened_at at now — a stale backlogged event should not
120
120
  # push opened_at into the past (which would trigger instant
121
121
  # max_duration closure on the next cycle).
122
- now = datetime.now(timezone.utc)
122
+ now = datetime.now(UTC)
123
123
  self._windows[domain] = SessionWindow(
124
124
  domain=domain,
125
125
  events=[],
@@ -141,7 +141,7 @@ class SessionWindowManager:
141
141
  def close_ready(self, now: datetime | None = None) -> list[SessionWindow]:
142
142
  """Close and return all windows that meet their close conditions."""
143
143
  if now is None:
144
- now = datetime.now(timezone.utc)
144
+ now = datetime.now(UTC)
145
145
 
146
146
  closed: list[SessionWindow] = []
147
147
  for domain in list(self._windows):
@@ -182,8 +182,8 @@ def _parse_event_ts(event: SitRepEvent) -> datetime:
182
182
  """Parse event timestamp to datetime. Always timezone-aware (UTC)."""
183
183
  ts = event.timestamp
184
184
  if isinstance(ts, datetime):
185
- return ts if ts.tzinfo else ts.replace(tzinfo=timezone.utc)
185
+ return ts if ts.tzinfo else ts.replace(tzinfo=UTC)
186
186
  dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
187
187
  if dt.tzinfo is None:
188
- dt = dt.replace(tzinfo=timezone.utc)
188
+ dt = dt.replace(tzinfo=UTC)
189
189
  return dt
@@ -51,9 +51,12 @@ class SnapshotGenerator:
51
51
  content_hash = self._compute_hash(groups)
52
52
 
53
53
  # Check cache (skip in INCIDENT mode)
54
- if not force_refresh and state != AgentState.INCIDENT:
55
- if self._is_cache_valid(content_hash, state):
56
- return self._cached_prompt or "", True
54
+ if (
55
+ not force_refresh
56
+ and state != AgentState.INCIDENT
57
+ and self._is_cache_valid(content_hash, state)
58
+ ):
59
+ return self._cached_prompt or "", True
57
60
 
58
61
  # Sort groups by priority (P0 first)
59
62
  sorted_groups = sorted(groups, key=lambda g: g.priority)
@@ -13,12 +13,12 @@ from __future__ import annotations
13
13
 
14
14
  import json
15
15
  import uuid
16
- from datetime import datetime, timezone
16
+ from datetime import UTC, datetime
17
17
  from pathlib import Path
18
18
 
19
19
  import structlog
20
-
21
20
  from nthlayer_common.prompts import load_prompt
21
+
22
22
  from nthlayer_workers.correlate.types import CorrelationGroup
23
23
  from nthlayer_workers.observe.assessment import Assessment
24
24
 
@@ -28,7 +28,7 @@ logger = structlog.get_logger()
28
28
 
29
29
 
30
30
  def _now() -> datetime:
31
- return datetime.now(timezone.utc)
31
+ return datetime.now(UTC)
32
32
 
33
33
 
34
34
  def _new_id(service: str | None) -> str:
@@ -155,6 +155,7 @@ class ModelInterface:
155
155
  async def _call_model(self, prompt: str) -> str:
156
156
  """Call the LLM via the shared nthlayer-common wrapper."""
157
157
  import asyncio
158
+
158
159
  from nthlayer_common.llm import llm_call
159
160
 
160
161
  result = await asyncio.to_thread(
@@ -1,7 +1,7 @@
1
1
  """Agent state machine for SitRep."""
2
2
  from __future__ import annotations
3
3
 
4
- from datetime import datetime, timezone
4
+ from datetime import UTC, datetime
5
5
 
6
6
  from nthlayer_workers.correlate.types import AgentState, CorrelationGroup
7
7
 
@@ -20,7 +20,7 @@ class StateMachine:
20
20
  model_healthy: bool = True,
21
21
  ) -> AgentState:
22
22
  """Evaluate state transition based on correlation output."""
23
- now = datetime.now(timezone.utc)
23
+ now = datetime.now(UTC)
24
24
 
25
25
  # Model failure → DEGRADED (from any state except INCIDENT)
26
26
  if not model_healthy and self.state != AgentState.INCIDENT:
@@ -65,7 +65,7 @@ class StateMachine:
65
65
  """Incident resolved → WATCHING."""
66
66
  self.state = AgentState.WATCHING
67
67
  self._alert_start = None
68
- self._calm_since = datetime.now(timezone.utc)
68
+ self._calm_since = datetime.now(UTC)
69
69
 
70
70
  def get_interval(self) -> int:
71
71
  """Snapshot interval in seconds for current state."""
@@ -11,7 +11,7 @@ import asyncio
11
11
  import os
12
12
  import time
13
13
  from dataclasses import dataclass, field
14
- from datetime import datetime, timedelta, timezone
14
+ from datetime import UTC, datetime, timedelta
15
15
 
16
16
  import httpx
17
17
 
@@ -631,7 +631,7 @@ class TempoTraceBackend:
631
631
  @staticmethod
632
632
  def _parse_tempo_timestamp(nanos: int) -> datetime:
633
633
  """Convert nanosecond unix timestamp to datetime."""
634
- return datetime.fromtimestamp(nanos / 1_000_000_000, tz=timezone.utc)
634
+ return datetime.fromtimestamp(nanos / 1_000_000_000, tz=UTC)
635
635
 
636
636
  @staticmethod
637
637
  def _traceql_service_filter(services: list[str]) -> str:
@@ -2,11 +2,11 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from dataclasses import dataclass, field
5
- from enum import Enum
5
+ from enum import StrEnum
6
6
  from typing import Any
7
7
 
8
8
 
9
- class EventType(str, Enum):
9
+ class EventType(StrEnum):
10
10
  ALERT = "alert"
11
11
  METRIC_BREACH = "metric_breach"
12
12
  CHANGE = "change"
@@ -70,7 +70,7 @@ class CorrelationGroup:
70
70
  event_count: int
71
71
 
72
72
 
73
- class AgentState(str, Enum):
73
+ class AgentState(StrEnum):
74
74
  WATCHING = "watching"
75
75
  ALERT = "alert"
76
76
  INCIDENT = "incident"
@@ -38,11 +38,9 @@ Deferred:
38
38
  from __future__ import annotations
39
39
 
40
40
  from dataclasses import dataclass, field
41
- from datetime import datetime, timedelta
42
- from typing import Any
41
+ from datetime import datetime
43
42
 
44
43
  import structlog
45
-
46
44
  from nthlayer_common.verdicts.models import Verdict
47
45
  from nthlayer_common.verdicts.store import VerdictFilter, VerdictStore
48
46
 
@@ -10,11 +10,10 @@ from __future__ import annotations
10
10
 
11
11
  import uuid
12
12
  from dataclasses import dataclass
13
- from datetime import datetime, timedelta, timezone
13
+ from datetime import UTC, datetime, timedelta
14
14
  from typing import Any
15
15
 
16
16
  import structlog
17
-
18
17
  from nthlayer_common.api_client import CoreAPIClient
19
18
  from nthlayer_common.cloudevents import wrap_assessment
20
19
 
@@ -147,7 +146,7 @@ class CorrelateSessionModule:
147
146
  # Orphaned window from crash recovery with no re-fetched events.
148
147
  # Skip rather than producing a phantom empty snapshot.
149
148
  return
150
- now = datetime.now(timezone.utc)
149
+ now = datetime.now(UTC)
151
150
  reason = window.close_reason(now, self.gap_seconds, self.max_duration_seconds)
152
151
 
153
152
  # Verdict IDs of QUALITY_SCORE events that triggered this snapshot.
@@ -222,7 +221,7 @@ def verdict_to_event(verdict: dict) -> SitRepEvent:
222
221
  """Convert a core verdict dict to a SitRepEvent."""
223
222
  return SitRepEvent(
224
223
  id=verdict.get("id", "unknown"),
225
- timestamp=verdict.get("created_at", datetime.now(timezone.utc).isoformat()),
224
+ timestamp=verdict.get("created_at", datetime.now(UTC).isoformat()),
226
225
  source=f"verdict:{verdict.get('type', 'unknown')}",
227
226
  type=EventType.QUALITY_SCORE if verdict.get("type") == "quality_breach" else EventType.VERDICT,
228
227
  service=verdict.get("service", "unknown"),
@@ -236,7 +235,7 @@ def assessment_to_event(assessment: dict) -> SitRepEvent:
236
235
  """Convert a core assessment dict to a SitRepEvent."""
237
236
  return SitRepEvent(
238
237
  id=assessment.get("id", "unknown"),
239
- timestamp=assessment.get("created_at", datetime.now(timezone.utc).isoformat()),
238
+ timestamp=assessment.get("created_at", datetime.now(UTC).isoformat()),
240
239
  source=f"assessment:{assessment.get('kind', 'unknown')}",
241
240
  type=EventType.METRIC_BREACH if _is_breach(assessment) else EventType.ALERT,
242
241
  service=assessment.get("service", "unknown"),
@@ -350,7 +349,7 @@ class CorrelateTopologyModule:
350
349
  # Get trace evidence for all services
351
350
  service_names = list(declared_deps.keys())
352
351
  try:
353
- end = datetime.now(timezone.utc)
352
+ end = datetime.now(UTC)
354
353
  start = end - timedelta(hours=1)
355
354
  evidence = await self.trace_backend.get_trace_evidence(
356
355
  services=service_names,
@@ -380,7 +379,7 @@ class CorrelateTopologyModule:
380
379
  return
381
380
 
382
381
  # Emit one assessment per divergence (org-level)
383
- now = datetime.now(timezone.utc)
382
+ now = datetime.now(UTC)
384
383
  assessment = {
385
384
  "id": f"tdr-topology-{uuid.uuid4().hex[:8]}",
386
385
  "created_at": now.isoformat(),
@@ -581,7 +580,7 @@ class CorrelateContractModule:
581
580
  if not divergences:
582
581
  continue
583
582
 
584
- now = datetime.now(timezone.utc)
583
+ now = datetime.now(UTC)
585
584
  assessment = {
586
585
  "id": f"cdv-{service}-{uuid.uuid4().hex[:8]}",
587
586
  "created_at": now.isoformat(),