pulse-engine 0.2.39__tar.gz → 0.2.42__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.
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/PKG-INFO +4 -1
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/pyproject.toml +14 -2
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/digital_news.py +62 -11
- pulse_engine-0.2.42/src/pulse_engine/adapters/google_sheets.py +292 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/providers/deepgram.py +1 -3
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/runners/prefect_pipeline_flow.py +26 -1
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/backend.py +9 -1
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/README.md +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/audio_transcription.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/batcher.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/digital_news_metadata.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/document_event_campaign_mapper.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/exceptions.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/mart_export.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/models.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/opensearch_sql.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/opensearch_storage.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/prompts.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/report_generator.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/speech_content.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/speech_metadata.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/base.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/config.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/factory.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/providers/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/providers/openai_whisper.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/providers/selfhosted_job.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/providers/youtube_captions.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/service.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/source.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/twitter.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/youtube_downloader.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/youtube_metadata.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/api/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/api/v1/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/api/v1/auth.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/api/v1/health.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/api/v1/router.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/main.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/cookiecutter.json +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/client.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/config.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/error_handlers.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/exceptions.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/job_token.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/logging.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/scope.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/core/security.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/models.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/pipeline_sync.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/repository.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/router.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/runner.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/schemas.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/service.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/tasks.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cron/translator.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/database.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/dependencies.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/deployment/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/deployment/infra_provisioner.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/evolution/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/evolution/adapter.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/evolution/client.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/evolution/models.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/evolution/router.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/evolution/service.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/extractor/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/extractor/adapters/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/extractor/base.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/main.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/__main__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/server.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/tools_kb.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/tools_modules.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/tools_pipelines.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/mcp/tools_processor.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/middleware/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/middleware/rate_limit.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/middleware/request_id.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/middleware/security_headers.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/middleware/tenant.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/notifications/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/notifications/apprise_notifier.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/config_parser.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/expression.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/models.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/repositories.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/router_modules.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/router_pipelines.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/schemas.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/service.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/translators/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/translators/airflow_status.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/translators/airflow_translator.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/translators/base.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/translators/prefect_status.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/pipeline/translators/prefect_translator.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/base.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/core/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/core/analysis.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/core/chunking.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/core/prompts.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/core/topic_splitter.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/defaults/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/defaults/core_processor.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/defaults/postprocessor.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/defaults/preprocessor.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/llm/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/llm/provider.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/ocr/gemini.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/pipeline.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/postprocessor/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/postprocessor/embeddings.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/postprocessor/tasks.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/preprocessor/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/preprocessor/tasks.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/router.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/processor/schemas.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/registry.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/agent.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/fixtures.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/models.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/prompts.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/service.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/staging.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/tools.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/validation.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/reporting/wiring.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/runners/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/runners/lambda_runner.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/runners/pipeline_runner.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/runners/prefect_runner.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/s3.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/lambda_microvm.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/microvm_agent/Dockerfile +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/microvm_agent/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/microvm_agent/server.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/sandbox/session.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/secrets.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/services/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/services/bootstrap.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/services/opensearch.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/catalog.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/models.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/repo.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/seed.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/seed_data/skill_files.json +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/seed_data/skills.json +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/sync.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/skills/tools.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/connectors/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/connectors/athena.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/connectors/base.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/connectors/opensearch.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/knowledge_base.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/router.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/storage/schemas.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/testing/__init__.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/testing/fixtures.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/testing/mocks.py +0 -0
- {pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/worker.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulse-engine
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.42
|
|
4
4
|
Summary: Pulse Engine — Hybrid framework for building Pulse products
|
|
5
5
|
Author: Pulse Team
|
|
6
6
|
Requires-Python: >=3.11,<3.13
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.11
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Provides-Extra: sheets
|
|
10
11
|
Provides-Extra: worker
|
|
11
12
|
Requires-Dist: alembic (>=1.14.0,<2.0.0)
|
|
12
13
|
Requires-Dist: apprise (==1.11.0)
|
|
@@ -23,6 +24,8 @@ Requires-Dist: deepagents (>=0.6,<0.7)
|
|
|
23
24
|
Requires-Dist: deepgram-sdk (>=3.7.0,<4.0.0)
|
|
24
25
|
Requires-Dist: email-validator (>=2.3.0,<3.0.0)
|
|
25
26
|
Requires-Dist: fastapi (>=0.115.0,<0.116.0)
|
|
27
|
+
Requires-Dist: google-auth (>=2.35,<3.0) ; extra == "sheets"
|
|
28
|
+
Requires-Dist: gspread (>=6.1,<7.0) ; extra == "sheets"
|
|
26
29
|
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
27
30
|
Requires-Dist: importlib-metadata (>=9.0.0,<10.0.0)
|
|
28
31
|
Requires-Dist: langchain (>=1.2.13,<2.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pulse-engine"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.42"
|
|
4
4
|
description = "Pulse Engine — Hybrid framework for building Pulse products"
|
|
5
5
|
authors = ["Pulse Team"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -53,10 +53,16 @@ langchain-agentcore-codeinterpreter = "^0.0.5"
|
|
|
53
53
|
bedrock-agentcore = "^1.17.0"
|
|
54
54
|
yt-dlp = "^2026.7.4"
|
|
55
55
|
openpyxl = "^3.1"
|
|
56
|
+
# Optional: only GoogleSheetsAdapter needs these, and it imports them lazily, so an
|
|
57
|
+
# install that never reads a spreadsheet does not pay for them. Enable with the
|
|
58
|
+
# "sheets" extra.
|
|
59
|
+
gspread = {version = "^6.1", optional = true}
|
|
60
|
+
google-auth = {version = "^2.35", optional = true}
|
|
56
61
|
|
|
57
62
|
|
|
58
63
|
[tool.poetry.extras]
|
|
59
64
|
worker = ["prefect"]
|
|
65
|
+
sheets = ["gspread", "google-auth"]
|
|
60
66
|
|
|
61
67
|
[tool.poetry.group.dev.dependencies]
|
|
62
68
|
pytest = "^8.3.0"
|
|
@@ -111,9 +117,15 @@ module = ["langdetect.*", "tiktoken.*", "bs4.*", "jose", "jose.*", "boto3", "bot
|
|
|
111
117
|
ignore_missing_imports = true
|
|
112
118
|
|
|
113
119
|
[[tool.mypy.overrides]]
|
|
114
|
-
module = ["prefect", "prefect.*", "entrypoint", "fitz", "twikit", "twikit.*", "yt_dlp", "yt_dlp.*", "google", "google.*", "apprise", "apprise.*", "deepagents", "deepagents.*", "langchain_agentcore_codeinterpreter", "langchain_agentcore_codeinterpreter.*", "bedrock_agentcore", "bedrock_agentcore.*", "openpyxl", "openpyxl.*"]
|
|
120
|
+
module = ["prefect", "prefect.*", "entrypoint", "fitz", "twikit", "twikit.*", "yt_dlp", "yt_dlp.*", "google", "google.*", "apprise", "apprise.*", "deepagents", "deepagents.*", "langchain_agentcore_codeinterpreter", "langchain_agentcore_codeinterpreter.*", "bedrock_agentcore", "bedrock_agentcore.*", "openpyxl", "openpyxl.*", "gspread", "gspread.*"]
|
|
115
121
|
ignore_missing_imports = true
|
|
116
122
|
|
|
123
|
+
[[tool.mypy.overrides]]
|
|
124
|
+
# google-auth ships no annotations for `from_service_account_info`, so the one call this
|
|
125
|
+
# adapter makes into it is untyped. Scoped to this module rather than pragma'd inline.
|
|
126
|
+
module = ["pulse_engine.adapters.google_sheets"]
|
|
127
|
+
disallow_untyped_calls = false
|
|
128
|
+
|
|
117
129
|
[[tool.mypy.overrides]]
|
|
118
130
|
module = ["scripts.*"]
|
|
119
131
|
ignore_errors = true
|
|
@@ -44,6 +44,11 @@ class _ArticleSchema(BaseModel):
|
|
|
44
44
|
content: str = Field(description="The full main article text content")
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class TransientFetchError(Exception):
|
|
48
|
+
"""Raised for any failure worth retrying: network issues, rate limits,
|
|
49
|
+
server errors, or a 404 that may be a stale CDN edge-cache response."""
|
|
50
|
+
|
|
51
|
+
|
|
47
52
|
class DigitalNewsAdapter:
|
|
48
53
|
"""Fetches article content via Cloudflare Browser Rendering + LLM.
|
|
49
54
|
|
|
@@ -65,13 +70,19 @@ class DigitalNewsAdapter:
|
|
|
65
70
|
adapter = DigitalNewsAdapter(llm=ChatOllama(model="llama3"))
|
|
66
71
|
"""
|
|
67
72
|
|
|
73
|
+
_RETRYABLE_TARGET_STATUSES = [404, 429, 500, 502, 503, 504]
|
|
74
|
+
|
|
68
75
|
def __init__(
|
|
69
76
|
self,
|
|
70
77
|
llm: Any,
|
|
71
78
|
cloudflare_timeout: float = 60.0,
|
|
79
|
+
max_retries: int = 3,
|
|
80
|
+
retry_delay: float = 5.0,
|
|
72
81
|
) -> None:
|
|
73
82
|
self._llm = llm
|
|
74
83
|
self._cloudflare_timeout = cloudflare_timeout
|
|
84
|
+
self._max_retries = max_retries
|
|
85
|
+
self._retry_delay = retry_delay
|
|
75
86
|
|
|
76
87
|
self._account_id = os.environ.get("CLOUDFLARE_ACCOUNT_ID")
|
|
77
88
|
if not self._account_id:
|
|
@@ -83,8 +94,30 @@ class DigitalNewsAdapter:
|
|
|
83
94
|
|
|
84
95
|
async def fetch(self, url: str) -> ArticleContent:
|
|
85
96
|
"""Fetch and extract content from a single article URL."""
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
url = url.strip()
|
|
98
|
+
last_exc: Exception | None = None
|
|
99
|
+
|
|
100
|
+
for attempt in range(1, self._max_retries + 1):
|
|
101
|
+
try:
|
|
102
|
+
markdown = await self._fetch_markdown(url)
|
|
103
|
+
return await self._extract_content(markdown)
|
|
104
|
+
except TransientFetchError as exc:
|
|
105
|
+
last_exc = exc
|
|
106
|
+
if attempt < self._max_retries:
|
|
107
|
+
logger.warning(
|
|
108
|
+
"fetch_retry", url=url, attempt=attempt, reason=str(exc)
|
|
109
|
+
)
|
|
110
|
+
await asyncio.sleep(self._retry_delay)
|
|
111
|
+
else:
|
|
112
|
+
logger.warning(
|
|
113
|
+
"fetch_retries_exhausted",
|
|
114
|
+
url=url,
|
|
115
|
+
attempts=attempt,
|
|
116
|
+
reason=str(exc),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert last_exc is not None
|
|
120
|
+
raise last_exc
|
|
88
121
|
|
|
89
122
|
async def fetch_many(
|
|
90
123
|
self,
|
|
@@ -114,20 +147,38 @@ class DigitalNewsAdapter:
|
|
|
114
147
|
async with httpx.AsyncClient(
|
|
115
148
|
timeout=self._cloudflare_timeout, follow_redirects=True
|
|
116
149
|
) as client:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
try:
|
|
151
|
+
resp = await client.post(
|
|
152
|
+
cf_url,
|
|
153
|
+
headers={
|
|
154
|
+
"Authorization": f"Bearer {self._bearer_token}",
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
},
|
|
157
|
+
json={"url": url},
|
|
158
|
+
)
|
|
159
|
+
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
160
|
+
raise TransientFetchError(
|
|
161
|
+
f"network error contacting Cloudflare for {url}: {exc}"
|
|
162
|
+
) from exc
|
|
163
|
+
|
|
164
|
+
# Rate limiting or server-side failure from Cloudflare's own API.
|
|
165
|
+
if resp.status_code == 429 or resp.status_code >= 500:
|
|
166
|
+
raise TransientFetchError(
|
|
167
|
+
f"Cloudflare API returned {resp.status_code} for {url}"
|
|
168
|
+
)
|
|
125
169
|
resp.raise_for_status()
|
|
126
170
|
|
|
127
171
|
data = resp.json()
|
|
172
|
+
|
|
173
|
+
target_status = (data.get("meta") or {}).get("status")
|
|
174
|
+
if target_status in self._RETRYABLE_TARGET_STATUSES:
|
|
175
|
+
raise TransientFetchError(
|
|
176
|
+
f"target page returned status {target_status} for {url}"
|
|
177
|
+
)
|
|
178
|
+
|
|
128
179
|
markdown: str = data.get("result", "")
|
|
129
180
|
if not markdown or not markdown.strip():
|
|
130
|
-
raise
|
|
181
|
+
raise TransientFetchError(f"Empty markdown returned by Cloudflare: {url}")
|
|
131
182
|
return markdown
|
|
132
183
|
|
|
133
184
|
async def _extract_content(self, markdown: str) -> ArticleContent:
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""GoogleSheetsAdapter — read-only access to a Google Sheets workbook.
|
|
2
|
+
|
|
3
|
+
Auth, quota handling and the API's rough edges live here; what the rows *mean* stays
|
|
4
|
+
with the product. A caller supplies a service-account key and a workbook id and gets
|
|
5
|
+
rows back — it never sees gspread, a credentials object, or a 429.
|
|
6
|
+
|
|
7
|
+
Two of those rough edges are worth naming, because both were learned from a running
|
|
8
|
+
system and neither is visible in the API:
|
|
9
|
+
|
|
10
|
+
* **Only rate limits are retried.** A quota rejection is transient and worth waiting
|
|
11
|
+
out; a missing tab or a revoked key is not, and retrying it six times turns a clear
|
|
12
|
+
failure into a slow one that reports the wrong cause. Anything that is not a rate
|
|
13
|
+
limit propagates on the first attempt.
|
|
14
|
+
* **Tab names drift.** The same logical tab is "Channel Codes" in one workbook and
|
|
15
|
+
"channel codes" in the next, because a human typed it. An exact match is tried
|
|
16
|
+
first, then a case- and whitespace-insensitive one, so a renamed tab does not fail
|
|
17
|
+
a run.
|
|
18
|
+
|
|
19
|
+
``gspread`` is imported lazily and declared under the ``sheets`` extra. It is a
|
|
20
|
+
dependency of this module, not of the engine, and an installation that never reads a
|
|
21
|
+
spreadsheet should not pay for it.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import time
|
|
29
|
+
from collections.abc import Callable, Iterator
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Read-only: this adapter has no write path, and the narrower scope means a leaked key
|
|
36
|
+
# cannot be used to modify a workbook.
|
|
37
|
+
READONLY_SCOPES = (
|
|
38
|
+
"https://www.googleapis.com/auth/spreadsheets.readonly",
|
|
39
|
+
"https://www.googleapis.com/auth/drive.readonly",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_RATE_LIMIT_MARKERS = ("429", "quota exceeded", "rate limit")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SheetError(Exception):
|
|
46
|
+
"""A spreadsheet could not be read: bad credentials, missing tab, bad workbook
|
|
47
|
+
id."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SheetRateLimitError(SheetError):
|
|
51
|
+
"""Quota rejected the read and the retries were exhausted.
|
|
52
|
+
|
|
53
|
+
Distinct from `SheetError` so a caller can tell "come back later" from "this will
|
|
54
|
+
never work", and alert differently on each.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_rate_limit(exc: Exception) -> bool:
|
|
59
|
+
message = str(exc).lower()
|
|
60
|
+
return any(marker in message for marker in _RATE_LIMIT_MARKERS)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class TabScan:
|
|
65
|
+
"""One worksheet seen during a scan.
|
|
66
|
+
|
|
67
|
+
`title` and `first_cell` are read as the scan reaches the tab; `values()` is
|
|
68
|
+
deferred, because the caller that needs this — a workbook whose tabs *are* the
|
|
69
|
+
records, keyed by a cell rather than by name — decides from `first_cell` whether
|
|
70
|
+
the rows are wanted. Reading every tab eagerly would spend a request on each one
|
|
71
|
+
it skips.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
title: str
|
|
75
|
+
first_cell: str
|
|
76
|
+
_values: Callable[[], list[list[Any]]]
|
|
77
|
+
error: str | None = None
|
|
78
|
+
"""Why this tab could not be read, or None.
|
|
79
|
+
|
|
80
|
+
Set instead of raising, because a workbook whose tabs are records is exactly the
|
|
81
|
+
place where one malformed tab must not cost the whole walk. A quota rejection is
|
|
82
|
+
different — that is workbook-wide, so it still propagates.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def values(self) -> list[list[Any]]:
|
|
86
|
+
"""The tab's rows, fetched on first ask."""
|
|
87
|
+
return self._values()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class GoogleSheetsAdapter:
|
|
91
|
+
"""Read worksheets from a Google Sheets workbook.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
credentials_json: Service-account key as a JSON string.
|
|
95
|
+
max_retries: Attempts before giving up on a rate-limited read. Default 6.
|
|
96
|
+
backoff_base_seconds: Base for exponential backoff between retries. Default 2.0,
|
|
97
|
+
giving 2s, 4s, 8s … — about two minutes over six attempts, which is
|
|
98
|
+
the order of a Sheets per-minute quota window.
|
|
99
|
+
sleep: Injected so tests can drive the backoff without waiting. Production
|
|
100
|
+
callers
|
|
101
|
+
leave this alone.
|
|
102
|
+
client: A pre-built gspread client, for tests. When given, `credentials_json` is
|
|
103
|
+
not used and no authorisation is attempted.
|
|
104
|
+
|
|
105
|
+
Usage::
|
|
106
|
+
|
|
107
|
+
adapter = GoogleSheetsAdapter(credentials_json=key)
|
|
108
|
+
rows = adapter.read_records(spreadsheet_id="1abc…", tab="Master")
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
credentials_json: str = "",
|
|
114
|
+
*,
|
|
115
|
+
max_retries: int = 6,
|
|
116
|
+
backoff_base_seconds: float = 2.0,
|
|
117
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
118
|
+
client: Any | None = None,
|
|
119
|
+
) -> None:
|
|
120
|
+
if max_retries < 1:
|
|
121
|
+
raise ValueError(f"max_retries must be at least 1, got {max_retries}")
|
|
122
|
+
self._credentials_json = credentials_json
|
|
123
|
+
self._max_retries = max_retries
|
|
124
|
+
self._backoff_base_seconds = backoff_base_seconds
|
|
125
|
+
self._sleep = sleep
|
|
126
|
+
self._client = client
|
|
127
|
+
|
|
128
|
+
# -- connection ----------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def client(self) -> Any:
|
|
132
|
+
"""The authorised gspread client, built once on first use."""
|
|
133
|
+
if self._client is None:
|
|
134
|
+
self._client = self._authorize(self._credentials_json)
|
|
135
|
+
return self._client
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _authorize(credentials_json: str) -> Any:
|
|
139
|
+
# Validated before gspread is imported: a blank or malformed key is a config
|
|
140
|
+
# error, and reporting it should not depend on the optional library being
|
|
141
|
+
# installed. It also fails faster, on the cheaper check.
|
|
142
|
+
if not credentials_json.strip():
|
|
143
|
+
raise SheetError("credentials_json is required to read a spreadsheet")
|
|
144
|
+
try:
|
|
145
|
+
info = json.loads(credentials_json)
|
|
146
|
+
except json.JSONDecodeError as exc:
|
|
147
|
+
raise SheetError(f"credentials_json is not valid JSON: {exc}") from exc
|
|
148
|
+
|
|
149
|
+
import gspread
|
|
150
|
+
from google.oauth2.service_account import Credentials
|
|
151
|
+
|
|
152
|
+
credentials = Credentials.from_service_account_info(
|
|
153
|
+
info, scopes=list(READONLY_SCOPES)
|
|
154
|
+
)
|
|
155
|
+
return gspread.authorize(credentials)
|
|
156
|
+
|
|
157
|
+
def _worksheet(self, spreadsheet_id: str, tab: str) -> Any:
|
|
158
|
+
try:
|
|
159
|
+
spreadsheet = self.client.open_by_key(spreadsheet_id)
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
if _is_rate_limit(exc):
|
|
162
|
+
raise
|
|
163
|
+
raise SheetError(f"cannot open workbook {spreadsheet_id!r}: {exc}") from exc
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
return spreadsheet.worksheet(tab)
|
|
167
|
+
except Exception:
|
|
168
|
+
# Tab names drift in case and spacing between workbooks; a relaxed match
|
|
169
|
+
# before giving up keeps a renamed "Channel Codes" from failing the run.
|
|
170
|
+
wanted = tab.strip().lower()
|
|
171
|
+
for worksheet in spreadsheet.worksheets():
|
|
172
|
+
if str(worksheet.title).strip().lower() == wanted:
|
|
173
|
+
return worksheet
|
|
174
|
+
raise SheetError(f"tab {tab!r} not found in the workbook") from None
|
|
175
|
+
|
|
176
|
+
def _with_retry(self, what: str, call: Callable[[], Any]) -> Any:
|
|
177
|
+
"""Run `call`, waiting out rate limits and nothing else."""
|
|
178
|
+
last_error: Exception | None = None
|
|
179
|
+
for attempt in range(self._max_retries):
|
|
180
|
+
try:
|
|
181
|
+
return call()
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
if not _is_rate_limit(exc):
|
|
184
|
+
raise
|
|
185
|
+
last_error = exc
|
|
186
|
+
delay = self._backoff_base_seconds * (2**attempt)
|
|
187
|
+
logger.warning(
|
|
188
|
+
"sheets rate limited on %s (attempt %d/%d), sleeping %.2fs",
|
|
189
|
+
what,
|
|
190
|
+
attempt + 1,
|
|
191
|
+
self._max_retries,
|
|
192
|
+
delay,
|
|
193
|
+
)
|
|
194
|
+
self._sleep(delay)
|
|
195
|
+
|
|
196
|
+
raise SheetRateLimitError(
|
|
197
|
+
f"quota rejected the read of {what} after {self._max_retries} attempts: "
|
|
198
|
+
f"{last_error}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# -- reads ---------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def read_records(self, spreadsheet_id: str, tab: str) -> list[dict[str, Any]]:
|
|
204
|
+
"""One worksheet as a list of row dicts, keyed by the header row.
|
|
205
|
+
|
|
206
|
+
`expected_headers=[]` disables gspread's duplicate-header check: a tracker
|
|
207
|
+
maintained by hand routinely has two blank or repeated column titles, and the
|
|
208
|
+
caller decides which columns it cares about anyway.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def _read() -> list[dict[str, Any]]:
|
|
212
|
+
worksheet = self._worksheet(spreadsheet_id, tab)
|
|
213
|
+
rows: list[dict[str, Any]] = worksheet.get_all_records(expected_headers=[])
|
|
214
|
+
return rows
|
|
215
|
+
|
|
216
|
+
return list(self._with_retry(repr(tab), _read))
|
|
217
|
+
|
|
218
|
+
def read_values(self, spreadsheet_id: str, tab: str) -> list[list[Any]]:
|
|
219
|
+
"""One worksheet as raw rows, header included and nothing coerced."""
|
|
220
|
+
|
|
221
|
+
def _read() -> list[list[Any]]:
|
|
222
|
+
worksheet = self._worksheet(spreadsheet_id, tab)
|
|
223
|
+
values: list[list[Any]] = worksheet.get_all_values()
|
|
224
|
+
return values
|
|
225
|
+
|
|
226
|
+
return list(self._with_retry(repr(tab), _read))
|
|
227
|
+
|
|
228
|
+
def read_first_cell(self, spreadsheet_id: str, tab: str) -> str:
|
|
229
|
+
"""A1 of one worksheet, empty string when the sheet is blank.
|
|
230
|
+
|
|
231
|
+
Cheap enough to probe a tab's identity without pulling every row.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def _read() -> str:
|
|
235
|
+
worksheet = self._worksheet(spreadsheet_id, tab)
|
|
236
|
+
return str(worksheet.acell("A1").value or "")
|
|
237
|
+
|
|
238
|
+
return str(self._with_retry(repr(tab), _read))
|
|
239
|
+
|
|
240
|
+
def list_worksheet_titles(self, spreadsheet_id: str) -> list[str]:
|
|
241
|
+
"""Every tab title in the workbook, in sheet order."""
|
|
242
|
+
|
|
243
|
+
def _read() -> list[str]:
|
|
244
|
+
spreadsheet = self.client.open_by_key(spreadsheet_id)
|
|
245
|
+
return [str(w.title) for w in spreadsheet.worksheets()]
|
|
246
|
+
|
|
247
|
+
return list(self._with_retry(f"workbook {spreadsheet_id!r}", _read))
|
|
248
|
+
|
|
249
|
+
def scan_tabs(self, spreadsheet_id: str) -> Iterator[TabScan]:
|
|
250
|
+
"""Walk every tab in the workbook from a single open.
|
|
251
|
+
|
|
252
|
+
For a workbook that keys its records by a cell rather than by tab name, the
|
|
253
|
+
tab name is not known ahead of time, so `read_records` cannot address it.
|
|
254
|
+
Addressing tabs by name would also re-open the workbook per call, turning one
|
|
255
|
+
open into roughly 2N for N tabs — the wrong trade against a read quota.
|
|
256
|
+
|
|
257
|
+
Yields lazily so a caller can stop early, and defers the row read so a tab it
|
|
258
|
+
skips costs one cell rather than a whole sheet.
|
|
259
|
+
"""
|
|
260
|
+
spreadsheet = self._with_retry(
|
|
261
|
+
f"workbook {spreadsheet_id!r}",
|
|
262
|
+
lambda: self.client.open_by_key(spreadsheet_id),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
for worksheet in spreadsheet.worksheets():
|
|
266
|
+
title = str(worksheet.title)
|
|
267
|
+
|
|
268
|
+
def _first(w: Any = worksheet, t: str = title) -> str:
|
|
269
|
+
return str(self._with_retry(repr(t), lambda: w.acell("A1").value or ""))
|
|
270
|
+
|
|
271
|
+
def _values(w: Any = worksheet, t: str = title) -> list[list[Any]]:
|
|
272
|
+
rows: list[list[Any]] = list(
|
|
273
|
+
self._with_retry(repr(t), lambda: w.get_all_values())
|
|
274
|
+
)
|
|
275
|
+
return rows
|
|
276
|
+
|
|
277
|
+
# A tab that cannot be read is reported, not raised. Raising would end
|
|
278
|
+
# the generator and take every remaining tab with it — and a generator
|
|
279
|
+
# cannot be resumed past an exception, so the caller could not skip one
|
|
280
|
+
# bad tab even if it wanted to. A rate limit is not a per-tab problem and
|
|
281
|
+
# still propagates from `_with_retry` once its retries are spent.
|
|
282
|
+
try:
|
|
283
|
+
first_cell, error = _first(), None
|
|
284
|
+
except SheetRateLimitError:
|
|
285
|
+
raise
|
|
286
|
+
except Exception as exc: # noqa: BLE001 — reported per tab, not swallowed
|
|
287
|
+
logger.warning("sheets could not read A1 of %r: %s", title, exc)
|
|
288
|
+
first_cell, error = "", str(exc)
|
|
289
|
+
|
|
290
|
+
yield TabScan(
|
|
291
|
+
title=title, first_cell=first_cell, _values=_values, error=error
|
|
292
|
+
)
|
|
@@ -53,9 +53,7 @@ class DeepgramProvider(TranscriptionProvider):
|
|
|
53
53
|
# back at the caller and discard the one useful output of detection —
|
|
54
54
|
# which is what lets a caller assert the source language was plausible.
|
|
55
55
|
detected = str(
|
|
56
|
-
alt.get("language")
|
|
57
|
-
or channel.get("detected_language")
|
|
58
|
-
or language
|
|
56
|
+
alt.get("language") or channel.get("detected_language") or language
|
|
59
57
|
)
|
|
60
58
|
|
|
61
59
|
utterances = results.get("utterances") or []
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/runners/prefect_pipeline_flow.py
RENAMED
|
@@ -40,9 +40,34 @@ logger = logging.getLogger(__name__)
|
|
|
40
40
|
|
|
41
41
|
# Runner script injected as the container CMD for both ECS and VM dispatch.
|
|
42
42
|
# Bypasses awslambdaric (Lambda-only) and runs the product entrypoint directly.
|
|
43
|
+
# Steps on ECS, Fargate and MicroVMs run the script below under `python -c`, so they
|
|
44
|
+
# never reach a product's Lambda handler and inherit no logging setup. Without this the
|
|
45
|
+
# root logger sits at its default WARNING with no handler, and every logger.info a
|
|
46
|
+
# module writes is discarded by logging.lastResort, which emits WARNING and above only.
|
|
47
|
+
#
|
|
48
|
+
# stderr, explicitly: the runner prints the step's JSON result on stdout and the MicroVM
|
|
49
|
+
# path captures that stream, so a log line written there would land inside the payload.
|
|
50
|
+
#
|
|
51
|
+
# Configures only a process that configured nothing. An absent handler is what marks
|
|
52
|
+
# the default state, so it is also the signal that no one has chosen a verbosity: a
|
|
53
|
+
# caller who called basicConfig(level=ERROR) meant it, and raising that to INFO would
|
|
54
|
+
# overrule the decision while a second handler duplicated every line.
|
|
55
|
+
#
|
|
56
|
+
# The Lambda handler in each product reads AWS_LAMBDA_LOG_LEVEL for the same purpose,
|
|
57
|
+
# because awslambdaric always installs a handler and absence cannot be the signal there.
|
|
58
|
+
_LOGGING_PREAMBLE = (
|
|
59
|
+
"import logging as _logging, sys as _sys\n"
|
|
60
|
+
"_root = _logging.getLogger()\n"
|
|
61
|
+
"if not _root.handlers:\n"
|
|
62
|
+
" _root.addHandler(_logging.StreamHandler(_sys.stderr))\n"
|
|
63
|
+
" if _root.getEffectiveLevel() > _logging.INFO:\n"
|
|
64
|
+
" _root.setLevel(_logging.INFO)\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
43
67
|
_VM_RUNNER_SCRIPT = (
|
|
44
68
|
"import os, json, sys, traceback, boto3\n"
|
|
45
|
-
|
|
69
|
+
+ _LOGGING_PREAMBLE
|
|
70
|
+
+ "sys.path.insert(0, '/app')\n"
|
|
46
71
|
"sys.path.insert(0, '/app/src')\n"
|
|
47
72
|
"try:\n"
|
|
48
73
|
" from botocore.exceptions import ClientError as _CE\n"
|
|
@@ -85,7 +85,15 @@ def make_lambda_microvm_backend(
|
|
|
85
85
|
if client is None: # pragma: no cover — real AWS path
|
|
86
86
|
import boto3
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
# `lambda-microvms` is newer than the pinned boto3-stubs, which type
|
|
89
|
+
# `client()` as an overload set over known service names. The pinned stubs
|
|
90
|
+
# reject the literal; a newer installed set accepts it. A `type: ignore` is
|
|
91
|
+
# therefore wrong in one direction or the other: absent, CI fails on the
|
|
92
|
+
# unknown overload; present, it fails locally as an unused ignore, because
|
|
93
|
+
# warn_unused_ignores is on. Calling through an untyped reference sidesteps
|
|
94
|
+
# overload resolution entirely and holds under both.
|
|
95
|
+
make_client: Any = boto3.client
|
|
96
|
+
client = make_client("lambda-microvms", region_name=region)
|
|
89
97
|
|
|
90
98
|
# Ship the resumed VM's stdout to CloudWatch. Without a `logging` config the
|
|
91
99
|
# microVM's runtime output goes nowhere (no log group is auto-created), so the
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/audio_transcription.py
RENAMED
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/digital_news_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/config.py
RENAMED
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/service.py
RENAMED
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/adapters/transcription/source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulse_engine-0.2.39 → pulse_engine-0.2.42}/src/pulse_engine/cli/templates/cookiecutter.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|