pulse-engine 0.2.14__tar.gz → 0.2.15__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 (151) hide show
  1. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/PKG-INFO +1 -1
  2. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/pyproject.toml +2 -2
  3. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/youtube_downloader.py +42 -21
  4. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/api/v1/auth.py +1 -2
  5. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/config.py +4 -0
  6. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/main.py +1 -1
  7. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/translators/prefect_translator.py +29 -0
  8. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/runners/prefect_pipeline_flow.py +10 -0
  9. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/services/bootstrap.py +6 -1
  10. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/README.md +0 -0
  11. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/__init__.py +0 -0
  12. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/__init__.py +0 -0
  13. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/audio_transcription.py +0 -0
  14. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/batcher.py +0 -0
  15. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/digital_news.py +0 -0
  16. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
  17. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/exceptions.py +0 -0
  18. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/models.py +0 -0
  19. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
  20. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/speech_content.py +0 -0
  21. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/speech_metadata.py +0 -0
  22. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/__init__.py +0 -0
  23. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/base.py +0 -0
  24. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/config.py +0 -0
  25. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/factory.py +0 -0
  26. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/providers/__init__.py +0 -0
  27. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/providers/deepgram.py +0 -0
  28. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/providers/openai_whisper.py +0 -0
  29. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/providers/selfhosted_job.py +0 -0
  30. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/providers/youtube_captions.py +0 -0
  31. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/service.py +0 -0
  32. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/transcription/source.py +0 -0
  33. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/twitter.py +0 -0
  34. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
  35. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/api/__init__.py +0 -0
  36. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/api/v1/__init__.py +0 -0
  37. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/api/v1/health.py +0 -0
  38. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/api/v1/router.py +0 -0
  39. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/__init__.py +0 -0
  40. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/main.py +0 -0
  41. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
  42. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
  43. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
  44. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
  45. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
  46. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +0 -0
  47. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
  48. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
  49. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +0 -0
  50. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/client.py +0 -0
  51. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/__init__.py +0 -0
  52. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/error_handlers.py +0 -0
  53. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/exceptions.py +0 -0
  54. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/job_token.py +0 -0
  55. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/logging.py +0 -0
  56. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/scope.py +0 -0
  57. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/core/security.py +0 -0
  58. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/__init__.py +0 -0
  59. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/models.py +0 -0
  60. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/pipeline_sync.py +0 -0
  61. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/repository.py +0 -0
  62. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/router.py +0 -0
  63. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/runner.py +0 -0
  64. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/schemas.py +0 -0
  65. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/service.py +0 -0
  66. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/tasks.py +0 -0
  67. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/cron/translator.py +0 -0
  68. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/database.py +0 -0
  69. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/dependencies.py +0 -0
  70. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/deployment/__init__.py +0 -0
  71. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/deployment/infra_provisioner.py +0 -0
  72. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/evolution/__init__.py +0 -0
  73. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/evolution/adapter.py +0 -0
  74. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/evolution/client.py +0 -0
  75. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/evolution/models.py +0 -0
  76. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/evolution/router.py +0 -0
  77. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/evolution/service.py +0 -0
  78. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/extractor/__init__.py +0 -0
  79. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
  80. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/extractor/base.py +0 -0
  81. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/__init__.py +0 -0
  82. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/__main__.py +0 -0
  83. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/server.py +0 -0
  84. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/tools_kb.py +0 -0
  85. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/tools_modules.py +0 -0
  86. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
  87. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/mcp/tools_processor.py +0 -0
  88. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/middleware/__init__.py +0 -0
  89. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/middleware/rate_limit.py +0 -0
  90. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/middleware/request_id.py +0 -0
  91. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/middleware/security_headers.py +0 -0
  92. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/middleware/tenant.py +0 -0
  93. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/notifications/__init__.py +0 -0
  94. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
  95. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/__init__.py +0 -0
  96. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/config_parser.py +0 -0
  97. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/expression.py +0 -0
  98. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/models.py +0 -0
  99. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/repositories.py +0 -0
  100. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/router_modules.py +0 -0
  101. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/router_pipelines.py +0 -0
  102. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/schemas.py +0 -0
  103. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/service.py +0 -0
  104. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
  105. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
  106. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
  107. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/translators/base.py +0 -0
  108. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
  109. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/__init__.py +0 -0
  110. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/base.py +0 -0
  111. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/core/__init__.py +0 -0
  112. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/core/analysis.py +0 -0
  113. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/core/chunking.py +0 -0
  114. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/core/prompts.py +0 -0
  115. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
  116. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/defaults/__init__.py +0 -0
  117. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
  118. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
  119. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
  120. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/llm/__init__.py +0 -0
  121. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/llm/provider.py +0 -0
  122. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/ocr/gemini.py +0 -0
  123. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/pipeline.py +0 -0
  124. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
  125. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
  126. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
  127. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
  128. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
  129. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/router.py +0 -0
  130. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/processor/schemas.py +0 -0
  131. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/registry.py +0 -0
  132. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/runners/__init__.py +0 -0
  133. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/runners/lambda_runner.py +0 -0
  134. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/runners/pipeline_runner.py +0 -0
  135. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/runners/prefect_runner.py +0 -0
  136. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/s3.py +0 -0
  137. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/secrets.py +0 -0
  138. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/services/__init__.py +0 -0
  139. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/services/opensearch.py +0 -0
  140. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/__init__.py +0 -0
  141. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/connectors/__init__.py +0 -0
  142. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/connectors/athena.py +0 -0
  143. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/connectors/base.py +0 -0
  144. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
  145. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/knowledge_base.py +0 -0
  146. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/router.py +0 -0
  147. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/storage/schemas.py +0 -0
  148. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/testing/__init__.py +0 -0
  149. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/testing/fixtures.py +0 -0
  150. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/src/pulse_engine/testing/mocks.py +0 -0
  151. {pulse_engine-0.2.14 → pulse_engine-0.2.15}/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.14
3
+ Version: 0.2.15
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.14"
3
+ version = "0.2.15"
4
4
  description = "Pulse Engine — Hybrid framework for building Pulse products"
5
5
  authors = ["Pulse Team"]
6
6
  readme = "README.md"
@@ -107,7 +107,7 @@ module = ["langdetect.*", "tiktoken.*", "bs4.*", "jose", "jose.*", "boto3", "bot
107
107
  ignore_missing_imports = true
108
108
 
109
109
  [[tool.mypy.overrides]]
110
- module = ["prefect", "prefect.*", "entrypoint", "fitz", "twikit", "twikit.*", "yt_dlp", "google", "google.*", "apprise", "apprise.*"]
110
+ module = ["prefect", "prefect.*", "entrypoint", "fitz", "twikit", "twikit.*", "yt_dlp", "yt_dlp.*", "google", "google.*", "apprise", "apprise.*"]
111
111
  ignore_missing_imports = true
112
112
 
113
113
  [[tool.mypy.overrides]]
@@ -21,6 +21,8 @@ _DEFAULT_PLAYER_CLIENTS = ["tv_embedded", "web"]
21
21
  def _build_yt_dlp_opts(
22
22
  output_template: str,
23
23
  extra: dict[str, Any] | None = None,
24
+ clip_start: float | None = None,
25
+ clip_end: float | None = None,
24
26
  ) -> dict[str, Any]:
25
27
  """Build yt-dlp option dict, merging env defaults with caller overrides."""
26
28
  raw_clients = os.environ.get("YT_DLP_PLAYER_CLIENTS", "").strip()
@@ -53,23 +55,28 @@ def _build_yt_dlp_opts(
53
55
  },
54
56
  }
55
57
 
56
- if not extra:
57
- return opts
58
-
59
- override = dict(extra)
60
- # cookies_content is handled separately — materialised to a temp file.
61
- override.pop("cookies_content", None)
62
- ea = override.pop("extractor_args", None)
63
- opts.update(override)
64
- if isinstance(ea, dict):
65
- existing = opts.get("extractor_args") or {}
66
- merged: dict[str, Any] = {**existing}
67
- for k, v in ea.items():
68
- if isinstance(v, dict) and isinstance(merged.get(k), dict):
69
- merged[k] = {**merged[k], **v}
70
- else:
71
- merged[k] = v
72
- opts["extractor_args"] = merged
58
+ if extra:
59
+ override = dict(extra)
60
+ # cookies_content is handled separately — materialised to a temp file.
61
+ override.pop("cookies_content", None)
62
+ ea = override.pop("extractor_args", None)
63
+ opts.update(override)
64
+ if isinstance(ea, dict):
65
+ existing = opts.get("extractor_args") or {}
66
+ merged: dict[str, Any] = {**existing}
67
+ for k, v in ea.items():
68
+ if isinstance(v, dict) and isinstance(merged.get(k), dict):
69
+ merged[k] = {**merged[k], **v}
70
+ else:
71
+ merged[k] = v
72
+ opts["extractor_args"] = merged
73
+
74
+ if clip_start is not None or clip_end is not None:
75
+ from yt_dlp.utils import download_range_func
76
+
77
+ start = clip_start if clip_start is not None else 0.0
78
+ end = clip_end if clip_end is not None else float("inf")
79
+ opts["download_ranges"] = download_range_func(None, [(start, end)])
73
80
 
74
81
  return opts
75
82
 
@@ -91,7 +98,13 @@ class YouTubeDownloader:
91
98
  self._aws_region: str = os.environ.get("AWS_REGION", "us-east-1").strip()
92
99
  self._cookies_cache: str | None = None # lazily populated
93
100
 
94
- async def download(self, video_id: str, output_dir: str) -> Path | None:
101
+ async def download(
102
+ self,
103
+ video_id: str,
104
+ output_dir: str,
105
+ start: float | None = None,
106
+ end: float | None = None,
107
+ ) -> Path | None:
95
108
  """Download audio for *video_id* into *output_dir*, returning path to mp3.
96
109
 
97
110
  Returns ``None`` if the download fails for any reason.
@@ -99,7 +112,7 @@ class YouTubeDownloader:
99
112
  url = f"https://www.youtube.com/watch?v={video_id}"
100
113
  logger.info("youtube_download_started", video_id=video_id)
101
114
 
102
- result = await self._download_audio(url, output_dir)
115
+ result = await self._download_audio(url, output_dir, start=start, end=end)
103
116
  if result is None:
104
117
  logger.warning("youtube_download_failed", video_id=video_id)
105
118
  else:
@@ -152,7 +165,13 @@ class YouTubeDownloader:
152
165
  )
153
166
  return cookies
154
167
 
155
- async def _download_audio(self, url: str, output_dir: str) -> Path | None:
168
+ async def _download_audio(
169
+ self,
170
+ url: str,
171
+ output_dir: str,
172
+ start: float | None = None,
173
+ end: float | None = None,
174
+ ) -> Path | None:
156
175
  """Download audio from a YouTube URL to *output_dir* via yt-dlp."""
157
176
  try:
158
177
  import yt_dlp
@@ -161,7 +180,9 @@ class YouTubeDownloader:
161
180
  return None
162
181
 
163
182
  output_template = str(Path(output_dir) / "%(id)s.%(ext)s")
164
- ydl_opts = _build_yt_dlp_opts(output_template, extra=self._yt_dlp_opts)
183
+ ydl_opts = _build_yt_dlp_opts(
184
+ output_template, extra=self._yt_dlp_opts, clip_start=start, clip_end=end
185
+ )
165
186
 
166
187
  # Resolve cookies (priority: Secrets Manager > env var).
167
188
  cookies_content = await self._resolve_cookies()
@@ -47,8 +47,7 @@ def _compute_secret_hash(email: str, client_id: str, client_secret: str) -> str:
47
47
  @router.post("/login", response_model=LoginResponse)
48
48
  async def login(request: Request, body: LoginRequest) -> LoginResponse:
49
49
  """Authenticate with email and password, returns JWT tokens."""
50
- # Strict per-IP rate limit: 5 attempts per 60 seconds
51
- check_rate_limit(request, scope="login", max_requests=5, window_seconds=60)
50
+ check_rate_limit(request, scope="login", max_requests=500, window_seconds=60)
52
51
  settings = get_settings()
53
52
 
54
53
  client_id = settings.cognito_app_client_id
@@ -51,6 +51,10 @@ class Settings(BaseSettings):
51
51
 
52
52
  # Prefect ECS backend
53
53
  prefect_ecs_work_pool_name: str = "pipeline-ecs"
54
+ # Fleet-wide cap on concurrently-running flow runs for the ECS work pool.
55
+ # Acts as the queue throttle: scheduled runs beyond this wait in Prefect's
56
+ # queue instead of all launching at once (and OOM-ing the workers).
57
+ prefect_work_pool_concurrency_limit: int = 100
54
58
  # ECR image used to run pipeline_flow in ECS work pool
55
59
  prefect_engine_image: str = ""
56
60
  # Override compute backend for all pipeline steps ("ecs", "local", etc.)
@@ -117,7 +117,7 @@ def create_app(
117
117
  )
118
118
  app.add_middleware(
119
119
  RateLimitMiddleware,
120
- max_requests=100,
120
+ max_requests=10000,
121
121
  window_seconds=60,
122
122
  )
123
123
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
+ import logging
4
5
  from typing import Any
5
6
 
6
7
  import httpx
@@ -8,6 +9,8 @@ import httpx
8
9
  from pulse_engine.pipeline.schemas import PipelineConfig, SecretRef
9
10
  from pulse_engine.pipeline.translators.base import BaseTranslator
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
11
14
 
12
15
  class PrefectTranslator(BaseTranslator):
13
16
  """Translates a PipelineConfig into a Prefect flow run submission."""
@@ -128,6 +131,32 @@ class PrefectTranslator(BaseTranslator):
128
131
  deploy_resp.raise_for_status()
129
132
  return str(deploy_resp.json()["id"])
130
133
 
134
+ async def ensure_work_pool_concurrency_limit(self, limit: int) -> None:
135
+ """Set the work pool's concurrency limit (idempotent).
136
+
137
+ This is the fleet-wide queue throttle: Prefect holds scheduled runs
138
+ beyond ``limit`` in its queue rather than dispatching them all at once,
139
+ which is what prevents workers from over-grabbing and OOM-ing. Failures
140
+ are swallowed (logged) so a Prefect hiccup can't block app startup.
141
+ """
142
+ client = self._get_client()
143
+ try:
144
+ resp = await client.patch(
145
+ f"{self._api_url}/work_pools/{self._work_pool_name}",
146
+ json={"concurrency_limit": limit},
147
+ )
148
+ resp.raise_for_status()
149
+ except Exception as exc: # noqa: BLE001 - best-effort, non-fatal at startup
150
+ logger.warning(
151
+ "Failed to set concurrency limit %s on work pool %s: %s",
152
+ limit,
153
+ self._work_pool_name,
154
+ exc,
155
+ )
156
+ finally:
157
+ if self._client is None:
158
+ await client.aclose()
159
+
131
160
  async def submit(
132
161
  self,
133
162
  pipeline_run_id: str,
@@ -25,6 +25,12 @@ from pulse_engine.secrets import fetch_secret
25
25
 
26
26
  _STDERR_LIMIT = int(os.getenv("PULSE_VM_STDERR_LIMIT", "4000"))
27
27
  _VM_DEBUG = os.getenv("PULSE_VM_DEBUG", "0") == "1"
28
+ # Safety cap applied to fan-out steps that omit `max_concurrency`, so a single
29
+ # step can't launch unbounded step tasks and overwhelm the pipeline cluster.
30
+ # Set to 0 to restore fully-unbounded default behaviour.
31
+ _DEFAULT_FANOUT_MAX_CONCURRENCY = int(
32
+ os.getenv("PULSE_DEFAULT_FANOUT_MAX_CONCURRENCY", "20")
33
+ )
28
34
 
29
35
  _F = TypeVar("_F", bound=Callable[..., Any])
30
36
 
@@ -1293,6 +1299,10 @@ def _execute_layer(
1293
1299
  # below is a no-op (it's already done). Net effect: sync mode
1294
1300
  # is unaffected by max_concurrency.
1295
1301
  max_c = step_config.get("max_concurrency")
1302
+ # When a step omits an explicit cap, fall back to the global safety
1303
+ # default so unbounded fan-out can't flood the pipeline cluster.
1304
+ if (not max_c or max_c <= 0) and _DEFAULT_FANOUT_MAX_CONCURRENCY > 0:
1305
+ max_c = _DEFAULT_FANOUT_MAX_CONCURRENCY
1296
1306
  n_items = len(items)
1297
1307
  if not max_c or max_c <= 0 or max_c >= n_items:
1298
1308
  futures = [
@@ -154,12 +154,17 @@ async def bootstrap_services(
154
154
  PrefectTranslator,
155
155
  )
156
156
 
157
- pipeline_translators["prefect"] = PrefectTranslator(
157
+ prefect_translator = PrefectTranslator(
158
158
  prefect_api_url=settings.prefect_api_url,
159
159
  prefect_api_key=settings.prefect_api_key,
160
160
  work_pool_name=settings.prefect_ecs_work_pool_name,
161
161
  engine_image=settings.prefect_engine_image,
162
162
  )
163
+ # Apply the fleet-wide queue throttle (idempotent, best-effort).
164
+ await prefect_translator.ensure_work_pool_concurrency_limit(
165
+ settings.prefect_work_pool_concurrency_limit
166
+ )
167
+ pipeline_translators["prefect"] = prefect_translator
163
168
  pipeline_status_providers["prefect"] = PrefectStatusProvider(
164
169
  prefect_api_url=settings.prefect_api_url,
165
170
  prefect_api_key=settings.prefect_api_key,
File without changes