pulse-engine 0.2.26__tar.gz → 0.2.27__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 (180) hide show
  1. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/PKG-INFO +1 -1
  2. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/pyproject.toml +1 -1
  3. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/models.py +3 -0
  4. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/providers/youtube_captions.py +30 -11
  5. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/service.py +13 -3
  6. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/youtube_downloader.py +56 -19
  7. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/runner.py +1 -1
  8. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/deployment/infra_provisioner.py +90 -6
  9. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/router_pipelines.py +1 -0
  10. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/schemas.py +1 -0
  11. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/service.py +3 -0
  12. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/runners/prefect_pipeline_flow.py +21 -0
  13. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/README.md +0 -0
  14. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/__init__.py +0 -0
  15. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/__init__.py +0 -0
  16. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/audio_transcription.py +0 -0
  17. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/batcher.py +0 -0
  18. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/digital_news.py +0 -0
  19. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
  20. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/document_event_campaign_mapper.py +0 -0
  21. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/exceptions.py +0 -0
  22. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/mart_export.py +0 -0
  23. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/opensearch_sql.py +0 -0
  24. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
  25. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/prompts.py +0 -0
  26. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/report_generator.py +0 -0
  27. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/speech_content.py +0 -0
  28. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/speech_metadata.py +0 -0
  29. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/__init__.py +0 -0
  30. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/base.py +0 -0
  31. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/config.py +0 -0
  32. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/factory.py +0 -0
  33. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/providers/__init__.py +0 -0
  34. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/providers/deepgram.py +0 -0
  35. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/providers/openai_whisper.py +0 -0
  36. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/providers/selfhosted_job.py +0 -0
  37. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/transcription/source.py +0 -0
  38. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/twitter.py +0 -0
  39. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
  40. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/api/__init__.py +0 -0
  41. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/api/v1/__init__.py +0 -0
  42. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/api/v1/auth.py +0 -0
  43. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/api/v1/health.py +0 -0
  44. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/api/v1/router.py +0 -0
  45. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/__init__.py +0 -0
  46. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/main.py +0 -0
  47. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
  48. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
  49. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
  50. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
  51. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
  52. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +0 -0
  53. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
  54. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
  55. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +0 -0
  56. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/client.py +0 -0
  57. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/config.py +0 -0
  58. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/__init__.py +0 -0
  59. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/error_handlers.py +0 -0
  60. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/exceptions.py +0 -0
  61. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/job_token.py +0 -0
  62. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/logging.py +0 -0
  63. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/scope.py +0 -0
  64. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/core/security.py +0 -0
  65. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/__init__.py +0 -0
  66. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/models.py +0 -0
  67. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/pipeline_sync.py +0 -0
  68. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/repository.py +0 -0
  69. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/router.py +0 -0
  70. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/schemas.py +0 -0
  71. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/service.py +0 -0
  72. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/tasks.py +0 -0
  73. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/cron/translator.py +0 -0
  74. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/database.py +0 -0
  75. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/dependencies.py +0 -0
  76. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/deployment/__init__.py +0 -0
  77. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/evolution/__init__.py +0 -0
  78. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/evolution/adapter.py +0 -0
  79. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/evolution/client.py +0 -0
  80. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/evolution/models.py +0 -0
  81. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/evolution/router.py +0 -0
  82. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/evolution/service.py +0 -0
  83. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/extractor/__init__.py +0 -0
  84. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
  85. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/extractor/base.py +0 -0
  86. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/main.py +0 -0
  87. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/__init__.py +0 -0
  88. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/__main__.py +0 -0
  89. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/server.py +0 -0
  90. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/tools_kb.py +0 -0
  91. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/tools_modules.py +0 -0
  92. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
  93. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/mcp/tools_processor.py +0 -0
  94. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/middleware/__init__.py +0 -0
  95. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/middleware/rate_limit.py +0 -0
  96. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/middleware/request_id.py +0 -0
  97. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/middleware/security_headers.py +0 -0
  98. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/middleware/tenant.py +0 -0
  99. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/notifications/__init__.py +0 -0
  100. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
  101. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/__init__.py +0 -0
  102. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/config_parser.py +0 -0
  103. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/expression.py +0 -0
  104. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/models.py +0 -0
  105. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/repositories.py +0 -0
  106. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/router_modules.py +0 -0
  107. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
  108. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
  109. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
  110. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/translators/base.py +0 -0
  111. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
  112. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/pipeline/translators/prefect_translator.py +0 -0
  113. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/__init__.py +0 -0
  114. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/base.py +0 -0
  115. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/core/__init__.py +0 -0
  116. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/core/analysis.py +0 -0
  117. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/core/chunking.py +0 -0
  118. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/core/prompts.py +0 -0
  119. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
  120. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/defaults/__init__.py +0 -0
  121. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
  122. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
  123. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
  124. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/llm/__init__.py +0 -0
  125. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/llm/provider.py +0 -0
  126. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/ocr/gemini.py +0 -0
  127. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/pipeline.py +0 -0
  128. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
  129. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
  130. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
  131. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
  132. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
  133. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/router.py +0 -0
  134. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/processor/schemas.py +0 -0
  135. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/registry.py +0 -0
  136. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/__init__.py +0 -0
  137. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/agent.py +0 -0
  138. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/fixtures.py +0 -0
  139. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/models.py +0 -0
  140. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/prompts.py +0 -0
  141. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/service.py +0 -0
  142. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/tools.py +0 -0
  143. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/reporting/wiring.py +0 -0
  144. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/runners/__init__.py +0 -0
  145. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/runners/lambda_runner.py +0 -0
  146. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/runners/pipeline_runner.py +0 -0
  147. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/runners/prefect_runner.py +0 -0
  148. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/s3.py +0 -0
  149. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/__init__.py +0 -0
  150. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/backend.py +0 -0
  151. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/lambda_microvm.py +0 -0
  152. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/microvm_agent/Dockerfile +0 -0
  153. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/microvm_agent/__init__.py +0 -0
  154. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/microvm_agent/server.py +0 -0
  155. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/sandbox/session.py +0 -0
  156. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/secrets.py +0 -0
  157. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/services/__init__.py +0 -0
  158. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/services/bootstrap.py +0 -0
  159. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/services/opensearch.py +0 -0
  160. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/__init__.py +0 -0
  161. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/catalog.py +0 -0
  162. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/models.py +0 -0
  163. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/repo.py +0 -0
  164. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/seed.py +0 -0
  165. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/seed_data/skill_files.json +0 -0
  166. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/seed_data/skills.json +0 -0
  167. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/sync.py +0 -0
  168. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/skills/tools.py +0 -0
  169. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/__init__.py +0 -0
  170. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/connectors/__init__.py +0 -0
  171. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/connectors/athena.py +0 -0
  172. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/connectors/base.py +0 -0
  173. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
  174. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/knowledge_base.py +0 -0
  175. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/router.py +0 -0
  176. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/storage/schemas.py +0 -0
  177. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/testing/__init__.py +0 -0
  178. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/testing/fixtures.py +0 -0
  179. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/testing/mocks.py +0 -0
  180. {pulse_engine-0.2.26 → pulse_engine-0.2.27}/src/pulse_engine/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pulse-engine
3
- Version: 0.2.26
3
+ Version: 0.2.27
4
4
  Summary: Pulse Engine — Hybrid framework for building Pulse products
5
5
  Author: Pulse Team
6
6
  Requires-Python: >=3.11,<3.13
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pulse-engine"
3
- version = "0.2.26"
3
+ version = "0.2.27"
4
4
  description = "Pulse Engine — Hybrid framework for building Pulse products"
5
5
  authors = ["Pulse Team"]
6
6
  readme = "README.md"
@@ -147,6 +147,9 @@ class VideoAudio:
147
147
  transcript: str
148
148
  language: str
149
149
  segments: list[TranscriptSegment] = field(default_factory=list)
150
+ provider: str | None = None
151
+ used_proxy: bool = False
152
+ error: str | None = None
150
153
 
151
154
 
152
155
  @dataclass
@@ -215,6 +215,23 @@ class YouTubeCaptionsProvider(TranscriptionProvider):
215
215
  raise TranscriptionUnavailable("yt-dlp not installed")
216
216
 
217
217
  video_id = source.video_id
218
+ proxy = os.environ.get("YT_DLP_PROXY", "").strip()
219
+
220
+ try:
221
+ result = await self._attempt(video_id, language, use_proxy=False)
222
+ result.used_proxy = False
223
+ return result
224
+ except TranscriptionUnavailable:
225
+ if not (proxy and self._cookies_secret_id):
226
+ raise
227
+ logger.info("youtube_captions_retry_with_proxy", video_id=video_id)
228
+ result = await self._attempt(video_id, language, use_proxy=True)
229
+ result.used_proxy = True
230
+ return result
231
+
232
+ async def _attempt(
233
+ self, video_id: str, language: str, use_proxy: bool
234
+ ) -> VideoAudio:
218
235
  raw_clients = os.environ.get("YT_DLP_PLAYER_CLIENTS", "").strip()
219
236
  clients: list[str] = (
220
237
  [c.strip() for c in raw_clients.split(",") if c.strip()]
@@ -236,17 +253,19 @@ class YouTubeCaptionsProvider(TranscriptionProvider):
236
253
 
237
254
  # Optional residential/mobile proxy — YouTube bot-gates datacenter
238
255
  # IPs regardless of cookies, so caption fetches from cloud runs need
239
- # the same proxy the audio downloader uses. Unset = unchanged.
240
- proxy = os.environ.get("YT_DLP_PROXY", "").strip()
241
- if proxy:
242
- base_opts["proxy"] = proxy
243
-
244
- cookies = await self._resolve_cookies()
245
- if cookies:
246
- cookies_path = tmp / "cookies.txt"
247
- cookies_path.write_text(cookies, encoding="utf-8")
248
- cookies_path.chmod(0o600)
249
- base_opts["cookiefile"] = str(cookies_path)
256
+ # the same proxy the audio downloader uses. Only used on retry
257
+ # after a direct (no-proxy, no-cookies) attempt fails.
258
+ if use_proxy:
259
+ proxy = os.environ.get("YT_DLP_PROXY", "").strip()
260
+ if proxy:
261
+ base_opts["proxy"] = proxy
262
+
263
+ cookies = await self._resolve_cookies()
264
+ if cookies:
265
+ cookies_path = tmp / "cookies.txt"
266
+ cookies_path.write_text(cookies, encoding="utf-8")
267
+ cookies_path.chmod(0o600)
268
+ base_opts["cookiefile"] = str(cookies_path)
250
269
 
251
270
  try:
252
271
  manual_langs, auto_langs = await self._list_available_langs(
@@ -40,22 +40,32 @@ class TranscriptionService:
40
40
  audio_resolver=audio_resolver,
41
41
  )
42
42
 
43
+ last_error: str | None = None
43
44
  for name in chain:
44
45
  try:
45
46
  provider = self._factory.get(name)
46
- return await provider.transcribe(source, cfg.language)
47
+ result = await provider.transcribe(source, cfg.language)
48
+ result.provider = name
49
+ return result
47
50
  except TranscriptionUnavailable as exc:
51
+ last_error = str(exc)
48
52
  logger.info(
49
53
  "transcription_provider_unavailable",
50
54
  provider=name,
51
55
  reason=str(exc),
52
56
  )
53
57
  continue
54
- except Exception:
58
+ except Exception as exc:
59
+ last_error = str(exc)
55
60
  logger.warning(
56
61
  "transcription_provider_failed", provider=name, exc_info=True
57
62
  )
58
63
  continue
59
64
 
60
65
  logger.warning("transcription_all_providers_failed", chain=chain)
61
- return VideoAudio(transcript="", language=cfg.language, segments=[])
66
+ return VideoAudio(
67
+ transcript="",
68
+ language=cfg.language,
69
+ segments=[],
70
+ error=f"all providers failed ({', '.join(chain)}): {last_error}",
71
+ )
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import os
7
+ from dataclasses import dataclass
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
 
@@ -17,6 +18,20 @@ logger = structlog.get_logger(__name__)
17
18
  # age-restricted videos when cookies are provided. Override via YT_DLP_PLAYER_CLIENTS.
18
19
  _DEFAULT_PLAYER_CLIENTS = ["tv_embedded", "web"]
19
20
 
21
+ # Cap fragment/segment retries on the proxy path — a flaky proxy shouldn't
22
+ # re-pull fragments repeatedly on the metered egress. Direct-path retries are
23
+ # unaffected (governed entirely by the caller's `extra` overrides).
24
+ _PROXY_MAX_RETRIES = 1
25
+
26
+
27
+ @dataclass
28
+ class DownloadResult:
29
+ """Outcome of a `YouTubeDownloader.download()` call."""
30
+
31
+ path: Path | None
32
+ used_proxy: bool
33
+ error: str | None = None
34
+
20
35
 
21
36
  def _build_yt_dlp_opts(
22
37
  output_template: str,
@@ -36,8 +51,9 @@ def _build_yt_dlp_opts(
36
51
  opts: dict[str, Any] = {
37
52
  # Cap source bitrate — output is always re-encoded to 64kbps mp3, so
38
53
  # pulling a higher-bitrate stream just to downsample it wastes egress
39
- # (metered on the proxy path).
40
- "format": "bestaudio[abr<=96]/bestaudio/best",
54
+ # (metered on the proxy path). Capped to match the 64kbps mp3 output
55
+ # exactly; fallback chain keeps degrading gracefully if unavailable.
56
+ "format": "bestaudio[abr<=64]/bestaudio/best",
41
57
  "outtmpl": output_template,
42
58
  "postprocessors": [
43
59
  {
@@ -87,6 +103,17 @@ def _build_yt_dlp_opts(
87
103
  merged[k] = v
88
104
  opts["extractor_args"] = merged
89
105
 
106
+ if use_proxy and "proxy" in opts:
107
+ # Cap retries on the metered proxy path regardless of caller overrides
108
+ # (e.g. feed's hardening opts default retries=2) — a flaky proxy
109
+ # shouldn't re-pull fragments repeatedly on billed egress.
110
+ opts["retries"] = min(
111
+ opts.get("retries", _PROXY_MAX_RETRIES), _PROXY_MAX_RETRIES
112
+ )
113
+ opts["fragment_retries"] = min(
114
+ opts.get("fragment_retries", _PROXY_MAX_RETRIES), _PROXY_MAX_RETRIES
115
+ )
116
+
90
117
  if clip_start is not None or clip_end is not None:
91
118
  from yt_dlp.utils import download_range_func
92
119
 
@@ -129,31 +156,33 @@ class YouTubeDownloader:
129
156
  output_dir: str,
130
157
  start: float | None = None,
131
158
  end: float | None = None,
132
- ) -> Path | None:
133
- """Download audio for *video_id* into *output_dir*, returning path to mp3.
159
+ ) -> DownloadResult:
160
+ """Download audio for *video_id* into *output_dir*.
134
161
 
135
- Returns ``None`` if the download fails for any reason.
162
+ Returns a `DownloadResult` — `path` is `None` if the download failed
163
+ for any reason, with `used_proxy`/`error` describing which attempt
164
+ ran and why it failed.
136
165
  """
137
166
  url = f"https://www.youtube.com/watch?v={video_id}"
138
167
  logger.info("youtube_download_started", video_id=video_id)
139
168
 
140
- result = await self._download_audio(
169
+ path, error = await self._download_audio(
141
170
  url, output_dir, start=start, end=end, use_proxy=False, use_cookies=False
142
171
  )
143
172
 
144
- if result is None and self._proxy_configured and self._cookies_secret_id:
173
+ used_proxy = False
174
+ if path is None and self._proxy_configured and self._cookies_secret_id:
145
175
  logger.info("youtube_download_retry_with_proxy", video_id=video_id)
146
- result = await self._download_audio(
176
+ used_proxy = True
177
+ path, error = await self._download_audio(
147
178
  url, output_dir, start=start, end=end, use_proxy=True, use_cookies=True
148
179
  )
149
180
 
150
- if result is None:
181
+ if path is None:
151
182
  logger.warning("youtube_download_failed", video_id=video_id)
152
183
  else:
153
- logger.info(
154
- "youtube_download_completed", video_id=video_id, path=str(result)
155
- )
156
- return result
184
+ logger.info("youtube_download_completed", video_id=video_id, path=str(path))
185
+ return DownloadResult(path=path, used_proxy=used_proxy, error=error)
157
186
 
158
187
  # ------------------------------------------------------------------
159
188
  # Private helpers
@@ -207,13 +236,17 @@ class YouTubeDownloader:
207
236
  end: float | None = None,
208
237
  use_proxy: bool = False,
209
238
  use_cookies: bool = False,
210
- ) -> Path | None:
211
- """Download audio from a YouTube URL to *output_dir* via yt-dlp."""
239
+ ) -> tuple[Path | None, str | None]:
240
+ """Download audio from a YouTube URL to *output_dir* via yt-dlp.
241
+
242
+ Returns `(path, error)` — `path` is `None` on failure, with `error`
243
+ carrying the failure reason (`None` on success).
244
+ """
212
245
  try:
213
246
  import yt_dlp
214
247
  except ImportError:
215
248
  logger.warning("yt_dlp_not_installed", url=url)
216
- return None
249
+ return None, "yt-dlp not installed"
217
250
 
218
251
  output_template = str(Path(output_dir) / "%(id)s.%(ext)s")
219
252
  ydl_opts = _build_yt_dlp_opts(
@@ -245,7 +278,11 @@ class YouTubeDownloader:
245
278
  return candidate if candidate.exists() else None
246
279
 
247
280
  try:
248
- return await asyncio.to_thread(_download)
249
- except Exception:
281
+ path = await asyncio.to_thread(_download)
282
+ except Exception as exc:
250
283
  logger.warning("youtube_audio_download_failed", url=url, exc_info=True)
251
- return None
284
+ return None, str(exc)
285
+
286
+ if path is None:
287
+ return None, "download completed but output file not found"
288
+ return path, None
@@ -81,7 +81,7 @@ def cron_job_flow(
81
81
  try:
82
82
  method = (webhook_method or "POST").upper()
83
83
  if method == "GET":
84
- resp = httpx.get(webhook_url, headers=webhook_headers, timeout=300.0)
84
+ resp = httpx.get(webhook_url, headers=webhook_headers, timeout=1800.0)
85
85
  else:
86
86
  resp = httpx.post(
87
87
  webhook_url,
@@ -13,6 +13,8 @@ Prerequisites (created by Terraform once):
13
13
  from __future__ import annotations
14
14
 
15
15
  import logging
16
+ import random
17
+ import time
16
18
  from typing import Any
17
19
 
18
20
  import boto3
@@ -20,6 +22,14 @@ from botocore.exceptions import ClientError
20
22
 
21
23
  logger = logging.getLogger(__name__)
22
24
 
25
+ # ECS throttles concurrent register_task_definition calls against the same
26
+ # family with this exact message. Retried with backoff since it's transient.
27
+ _ECS_CONCURRENT_REVISION_MESSAGE = (
28
+ "Too many concurrent attempts to create a new revision"
29
+ )
30
+ _ECS_REGISTER_MAX_RETRIES = 5
31
+ _ECS_REGISTER_BASE_DELAY_SECONDS = 1.0
32
+
23
33
 
24
34
  class InfraProvisioner:
25
35
  """Auto-provisions AWS infrastructure for pipeline step containers.
@@ -101,6 +111,36 @@ class InfraProvisioner:
101
111
  except ClientError as e:
102
112
  if e.response["Error"]["Code"] != "ClientException":
103
113
  raise
114
+ else:
115
+ # A specific image was requested — reuse the latest revision if
116
+ # it already matches (same image/cpu/memory) instead of blindly
117
+ # registering a new one every call. Concurrent pipeline runs
118
+ # otherwise all race to create new revisions of the same shared
119
+ # family, which ECS throttles with a ClientException.
120
+ try:
121
+ resp = ecs.describe_task_definition(taskDefinition=family)
122
+ existing = resp["taskDefinition"]
123
+ existing_containers = existing.get("containerDefinitions", [])
124
+ existing_image = (
125
+ existing_containers[0]["image"] if existing_containers else None
126
+ )
127
+ if (
128
+ existing_image == image
129
+ and existing.get("cpu") == cpu
130
+ and existing.get("memory") == memory
131
+ ):
132
+ result_arn = str(existing["taskDefinitionArn"])
133
+ logger.info(
134
+ "Reusing matching ECS task definition: %s (rev %s, image=%s)",
135
+ family,
136
+ existing["revision"],
137
+ image,
138
+ )
139
+ self._ecs_task_def_cache[cache_key] = result_arn
140
+ return result_arn
141
+ except ClientError as e:
142
+ if e.response["Error"]["Code"] != "ClientException":
143
+ raise
104
144
 
105
145
  # Register new task definition with the specified (or placeholder) image
106
146
  container_def: dict[str, Any] = {
@@ -124,15 +164,12 @@ class InfraProvisioner:
124
164
  },
125
165
  }
126
166
 
127
- resp = ecs.register_task_definition(
167
+ resp = self._register_task_definition_with_retry(
168
+ ecs,
128
169
  family=family,
129
- networkMode="awsvpc",
130
- requiresCompatibilities=["FARGATE"],
131
170
  cpu=cpu,
132
171
  memory=memory,
133
- executionRoleArn=self._exec_role_arn,
134
- taskRoleArn=self._task_role_arn,
135
- containerDefinitions=[container_def],
172
+ container_def=container_def,
136
173
  )
137
174
 
138
175
  result_arn = str(resp["taskDefinition"]["taskDefinitionArn"])
@@ -140,6 +177,53 @@ class InfraProvisioner:
140
177
  self._ecs_task_def_cache[cache_key] = result_arn
141
178
  return result_arn
142
179
 
180
+ def _register_task_definition_with_retry(
181
+ self,
182
+ ecs: Any,
183
+ *,
184
+ family: str,
185
+ cpu: str,
186
+ memory: str,
187
+ container_def: dict[str, Any],
188
+ ) -> dict[str, Any]:
189
+ """Call register_task_definition, retrying with backoff+jitter on the
190
+ ECS "too many concurrent attempts" throttle for this family."""
191
+ for attempt in range(1, _ECS_REGISTER_MAX_RETRIES + 1):
192
+ try:
193
+ return ecs.register_task_definition( # type: ignore[no-any-return]
194
+ family=family,
195
+ networkMode="awsvpc",
196
+ requiresCompatibilities=["FARGATE"],
197
+ cpu=cpu,
198
+ memory=memory,
199
+ executionRoleArn=self._exec_role_arn,
200
+ taskRoleArn=self._task_role_arn,
201
+ containerDefinitions=[container_def],
202
+ )
203
+ except ClientError as e:
204
+ error = e.response.get("Error", {})
205
+ is_concurrent_revision_throttle = (
206
+ error.get("Code") == "ClientException"
207
+ and _ECS_CONCURRENT_REVISION_MESSAGE in error.get("Message", "")
208
+ )
209
+ if (
210
+ not is_concurrent_revision_throttle
211
+ or attempt == _ECS_REGISTER_MAX_RETRIES
212
+ ):
213
+ raise
214
+ delay = _ECS_REGISTER_BASE_DELAY_SECONDS * (2 ** (attempt - 1))
215
+ delay += random.uniform(0, delay * 0.5)
216
+ logger.warning(
217
+ "ECS register_task_definition throttled for family %s "
218
+ "(attempt %d/%d), retrying in %.1fs",
219
+ family,
220
+ attempt,
221
+ _ECS_REGISTER_MAX_RETRIES,
222
+ delay,
223
+ )
224
+ time.sleep(delay)
225
+ raise AssertionError("unreachable") # loop always returns or raises
226
+
143
227
  # ------------------------------------------------------------------
144
228
  # Lambda Function
145
229
  # ------------------------------------------------------------------
@@ -192,6 +192,7 @@ async def init_pipeline_run(
192
192
  config_snapshot=body.config_snapshot,
193
193
  global_config=body.global_config,
194
194
  orchestrator=body.orchestrator,
195
+ orchestrator_run_id=body.orchestrator_run_id,
195
196
  )
196
197
  return InitRunResponse(run_id=run_id, token=token)
197
198
 
@@ -244,6 +244,7 @@ class InitRunRequest(BaseModel):
244
244
  config_snapshot: dict[str, Any]
245
245
  global_config: dict[str, Any] = Field(default_factory=dict)
246
246
  orchestrator: str = "prefect"
247
+ orchestrator_run_id: str | None = None
247
248
 
248
249
 
249
250
  class InitRunResponse(BaseModel):
@@ -315,6 +315,7 @@ class PipelineService:
315
315
  config_snapshot: dict[str, Any],
316
316
  global_config: dict[str, Any],
317
317
  orchestrator: str = "prefect",
318
+ orchestrator_run_id: str | None = None,
318
319
  ) -> tuple[str, str]:
319
320
  """Register a fresh pipeline run for a cron-triggered execution.
320
321
 
@@ -330,6 +331,8 @@ class PipelineService:
330
331
  global_config=global_config,
331
332
  )
332
333
  now = datetime.now(tz=UTC)
334
+ if orchestrator_run_id is not None:
335
+ await self._run_repo.set_orchestrator_run_id(run.id, orchestrator_run_id)
333
336
  await self._run_repo.update_status(run.id, "running", started_at=now)
334
337
 
335
338
  # Cron/scheduled per-execution runs may retry or run for days after the
@@ -15,6 +15,8 @@ import asyncio
15
15
  import json
16
16
  import logging
17
17
  import os
18
+ import re
19
+ import uuid
18
20
  from collections import deque
19
21
  from collections.abc import Callable
20
22
  from typing import Any, TypeVar
@@ -521,6 +523,13 @@ async def _dispatch_vm(
521
523
  step_name = env.get("STEP_NAME", "unknown")
522
524
  pipeline_run_id_val = env.get("PIPELINE_RUN_ID", "unknown")
523
525
 
526
+ # Give the container a stable, greppable name instead of Docker's random
527
+ # adjective_scientist default, so it shows up correctly in log shipping
528
+ # (promtail relabels containers named "pulse-*" to job=pulse-engine-logs).
529
+ _name_safe = re.sub(r"[^a-zA-Z0-9_.-]", "-", f"{step_name}-{pipeline_run_id_val}")
530
+ _short_id = uuid.uuid4().hex[:8]
531
+ container_name = f"pulse-engine-{_name_safe}-{_short_id}"[:128]
532
+
524
533
  try:
525
534
  from pulse_engine.config import get_settings
526
535
 
@@ -608,6 +617,7 @@ async def _dispatch_vm(
608
617
  "subprocess.run(['docker', 'pull', image], check=True)\n"
609
618
  "docker_cmd = [\n"
610
619
  " 'docker', 'run', '--rm',\n"
620
+ f" '--name', {repr(container_name)},\n"
611
621
  " '--memory', f'{memory_mb}m',\n"
612
622
  " '--cpus', str(cpu_count),\n"
613
623
  "]\n"
@@ -726,6 +736,16 @@ async def _dispatch_container(
726
736
  # ---------------------------------------------------------------------------
727
737
 
728
738
 
739
+ def _current_flow_run_id() -> str | None:
740
+ """Best-effort Prefect flow-run id for the flow currently executing."""
741
+ try:
742
+ from prefect.runtime import flow_run
743
+
744
+ return str(flow_run.id) if flow_run.id else None
745
+ except Exception:
746
+ return None
747
+
748
+
729
749
  def _init_cron_run(
730
750
  pulse_engine_url: str,
731
751
  pulse_api_token: str,
@@ -744,6 +764,7 @@ def _init_cron_run(
744
764
  "product": product,
745
765
  "config_snapshot": config_snapshot,
746
766
  "global_config": global_config,
767
+ "orchestrator_run_id": _current_flow_run_id(),
747
768
  },
748
769
  headers={"Authorization": f"Bearer {pulse_api_token}"},
749
770
  timeout=30.0,
File without changes