timber-common 1.2.2__tar.gz → 1.2.3__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.
- {timber_common-1.2.2 → timber_common-1.2.3}/CHANGELOG.md +12 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/PKG-INFO +1 -1
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/communication/__init__.py +2 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/communication/email_service.py +19 -10
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/communication/messaging_service.py +27 -35
- timber_common-1.2.3/common/services/communication/resend_provider.py +74 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/pyproject.toml +1 -1
- {timber_common-1.2.2 → timber_common-1.2.3}/LICENSE +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/README.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/cli/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/cli/commands/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/cli/commands/validate.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/cli/main.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/config/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/config/model_loader.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/engine/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/engine/config_executor.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/engine/operation_registry.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/init.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/a2ui.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/base.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/configs/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/core/__init.__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/core/enterprise.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/core/tag.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/core/user.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/factory.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/mixins.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/models/registry.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/plugins/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/plugins/contract.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/plugins/discovery.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/render/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/render/pdf.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/README.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/base.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/workflow/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/workflow/definition.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/workflow/enums.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/workflow/patterns.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/workflow/state.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/schemas/workflow/transition.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/a2a/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/a2a/client.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/communication/sms_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/db_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/decision_engine.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/expression_engine.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/graph_evaluator.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/models.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/table_evaluator.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/encryption/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/encryption/field_encryption.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/gdpr/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/gdpr/deletion.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/auth_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/integration_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/mapping_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/models.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/registry.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/inventory/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/inventory/available_capabilities.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/inventory/cached_capabilities.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/inventory/loader.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/batch.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/calibration.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/confidence.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/contract.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/guards.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/models.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/providers/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/providers/fixture.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/judgment/providers/typesafe.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/base.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/claude_provider.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/gemini_provider.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/groq_provider.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/model_choice.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm/perplexity_provider.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/llm_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/DIRECTORY_STRUCTURE.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/MIGRATION_GUIDE.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/QUICKSTART.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/README.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/VIDEO_README.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/config_helpers.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/examples.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/image_generation.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/model_resolver.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/video_examples.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/media/video_generation.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/base.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/cache.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/instances.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/manager.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/notification.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/research.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/session.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/persistence/tracker.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/registry.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/invalidator.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/user_context_cache.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/valkey_client.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/workflow_catalog_cache.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/security/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/security/oauth_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/vector/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/vector/auto_ingestion.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/vector/search.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/vector/tag_embedding.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/vendors/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/services/vendors/stripe_service.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/config.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/db_url.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/db_utils.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/decisioning/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/decisioning/csv_parser.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/decisioning/dmn_parser.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/decisioning/yaml_exporter.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/decisioning/yaml_parser.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/helpers.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/llm/chunk_aggregator.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/llm/content_condenser.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/llm/content_handler.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/llm/hierarchical_summarizer.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/serialization_helpers.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/time_helpers.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/common/utils/validators.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/data/models/00_association_tables.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/data/models/messaging_models.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/data/models/notification_models.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/data/models/oauth_models.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/data/models/user_preferences_models.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/data/models/vector_db_models.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/DOCUMENTATION_INDEX.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/DOCUMENTATION_SUMMARY.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/PROGRESS_UPDATE.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/TIMBER_SESSION_API_REFERENCE.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/01_model_design_patterns.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/02_service_architecture.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/03_data_fetching_strategies.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/04_caching_strategies.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/05_error_handling.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/06_performance_optimization.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/07_security_best_practices.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/01_system_architecture.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/02_config_driven_models.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/03_persistence_layer.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/04_vector_integration.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/05_multi_app_support.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/01_getting_started.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/02_creating_models.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/03_using_services.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/04_financial_data_fetching.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/05_encryption_and_security.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/06_vector_search.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/07_gdpr_compliance.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/08_testing_guide.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/09_typed_judgment.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/modules/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/modules/config/custom_analysis.yaml +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/README.md +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/__init__.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/bootstrap.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/cache.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/canopy_auth.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/client.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/env.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/models.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/specs.py +0 -0
- {timber_common-1.2.2 → timber_common-1.2.3}/oak_config/subscriber.py +0 -0
|
@@ -5,6 +5,18 @@ All notable changes to Timber will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.3] - 2026-10-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Resend transactional email transport using runtime-injected credentials and stable delivery identifiers.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Connect multichannel email delivery to the shared email service and scope message updates by ID.
|
|
17
|
+
- Record provider acceptance separately from delivery and reject unknown email providers.
|
|
18
|
+
- Select the default email provider lazily after runtime configuration loads.
|
|
19
|
+
|
|
8
20
|
## [1.2.2] - 2026-10-24
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: timber-common
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Configuration-driven persistence library with ml tools (finance related) config driven db model registration, llm model choice, automatic encryption, caching, vector search, and GDPR compliance for Python applications
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: orm,sqlalchemy,persistence,vector-search,semantic-search,encryption,gdpr,yaml-config,postgres,redis,configuration-driven,data-modeling,machine-learning,embeddings
|
|
@@ -64,6 +64,7 @@ from .email_service import (
|
|
|
64
64
|
SESProvider,
|
|
65
65
|
SMTPProvider,
|
|
66
66
|
ConsoleProvider,
|
|
67
|
+
ResendProvider,
|
|
67
68
|
get_email_service,
|
|
68
69
|
email_service,
|
|
69
70
|
)
|
|
@@ -103,6 +104,7 @@ __all__ = [
|
|
|
103
104
|
'SESProvider',
|
|
104
105
|
'SMTPProvider',
|
|
105
106
|
'ConsoleProvider',
|
|
107
|
+
'ResendProvider',
|
|
106
108
|
'get_email_service',
|
|
107
109
|
'email_service',
|
|
108
110
|
|
|
@@ -608,6 +608,9 @@ class ConsoleProvider(EmailProvider):
|
|
|
608
608
|
# EMAIL SERVICE
|
|
609
609
|
# =============================================================================
|
|
610
610
|
|
|
611
|
+
from .resend_provider import ResendProvider
|
|
612
|
+
|
|
613
|
+
|
|
611
614
|
class EmailService:
|
|
612
615
|
"""
|
|
613
616
|
Main email service with provider abstraction.
|
|
@@ -621,6 +624,7 @@ class EmailService:
|
|
|
621
624
|
'ses': SESProvider,
|
|
622
625
|
'smtp': SMTPProvider,
|
|
623
626
|
'console': ConsoleProvider,
|
|
627
|
+
'resend': ResendProvider,
|
|
624
628
|
}
|
|
625
629
|
|
|
626
630
|
def __init__(self, provider: Optional[str] = None):
|
|
@@ -631,7 +635,7 @@ class EmailService:
|
|
|
631
635
|
provider: Provider name ('sendgrid', 'ses', 'smtp', 'console')
|
|
632
636
|
If not specified, auto-detects from environment.
|
|
633
637
|
"""
|
|
634
|
-
self._provider_name = provider
|
|
638
|
+
self._provider_name = provider
|
|
635
639
|
self._provider: Optional[EmailProvider] = None
|
|
636
640
|
self._db = None
|
|
637
641
|
|
|
@@ -652,10 +656,10 @@ class EmailService:
|
|
|
652
656
|
def provider(self) -> EmailProvider:
|
|
653
657
|
"""Get the configured email provider."""
|
|
654
658
|
if self._provider is None:
|
|
659
|
+
self._provider_name = self._provider_name or os.getenv("EMAIL_PROVIDER", "console")
|
|
655
660
|
provider_class = self.PROVIDERS.get(self._provider_name)
|
|
656
661
|
if not provider_class:
|
|
657
|
-
|
|
658
|
-
provider_class = ConsoleProvider
|
|
662
|
+
raise ValueError(f"Unknown email provider: {self._provider_name}")
|
|
659
663
|
self._provider = provider_class()
|
|
660
664
|
return self._provider
|
|
661
665
|
|
|
@@ -724,14 +728,19 @@ class EmailService:
|
|
|
724
728
|
metadata=metadata or {},
|
|
725
729
|
)
|
|
726
730
|
|
|
727
|
-
|
|
731
|
+
return await self.send_message(message)
|
|
732
|
+
|
|
733
|
+
async def send_message(self, message: EmailMessage, *, log_delivery: bool = True) -> EmailResult:
|
|
734
|
+
"""Send a prepared message, preserving its identity across retries.
|
|
735
|
+
|
|
736
|
+
MessagingService owns its persisted delivery log and disables this log.
|
|
737
|
+
Provider acceptance is not proof of recipient delivery.
|
|
738
|
+
"""
|
|
728
739
|
result = await self.provider.send(message)
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
await self._log_email(message, result)
|
|
732
|
-
|
|
740
|
+
if log_delivery:
|
|
741
|
+
await self._log_email(message, result)
|
|
733
742
|
return result
|
|
734
|
-
|
|
743
|
+
|
|
735
744
|
async def send_template(
|
|
736
745
|
self,
|
|
737
746
|
to: Union[str, List[str], EmailAddress, List[EmailAddress]],
|
|
@@ -894,7 +903,7 @@ class EmailService:
|
|
|
894
903
|
},
|
|
895
904
|
'error_message': result.error,
|
|
896
905
|
'attempted_at': datetime.now(timezone.utc),
|
|
897
|
-
'delivered_at':
|
|
906
|
+
'delivered_at': None,
|
|
898
907
|
})
|
|
899
908
|
except Exception as e:
|
|
900
909
|
logger.debug(f"Failed to log email: {e}")
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/communication/messaging_service.py
RENAMED
|
@@ -277,7 +277,7 @@ class MessagingService:
|
|
|
277
277
|
message = self.db.get('Message', message_id)
|
|
278
278
|
|
|
279
279
|
if message and message['user_id'] == user_id:
|
|
280
|
-
self.db.update('Message', message_id, {
|
|
280
|
+
self.db.update('Message', {'id': message_id}, {
|
|
281
281
|
'in_app_status': 'read',
|
|
282
282
|
'in_app_read_at': datetime.now(timezone.utc),
|
|
283
283
|
'updated_at': datetime.now(timezone.utc),
|
|
@@ -298,7 +298,7 @@ class MessagingService:
|
|
|
298
298
|
count = 0
|
|
299
299
|
now = datetime.now(timezone.utc)
|
|
300
300
|
for msg in messages:
|
|
301
|
-
self.db.update('Message', msg['id'], {
|
|
301
|
+
self.db.update('Message', {'id': msg['id']}, {
|
|
302
302
|
'in_app_status': 'read',
|
|
303
303
|
'in_app_read_at': now,
|
|
304
304
|
'updated_at': now,
|
|
@@ -312,7 +312,7 @@ class MessagingService:
|
|
|
312
312
|
message = self.db.get('Message', message_id)
|
|
313
313
|
|
|
314
314
|
if message and message['user_id'] == user_id:
|
|
315
|
-
self.db.update('Message', message_id, {
|
|
315
|
+
self.db.update('Message', {'id': message_id}, {
|
|
316
316
|
'in_app_status': 'dismissed',
|
|
317
317
|
'updated_at': datetime.now(timezone.utc),
|
|
318
318
|
})
|
|
@@ -358,7 +358,7 @@ class MessagingService:
|
|
|
358
358
|
rendered: Dict[str, str],
|
|
359
359
|
) -> None:
|
|
360
360
|
"""Deliver in-app message (update status)."""
|
|
361
|
-
self.db.update('Message', message_id, {
|
|
361
|
+
self.db.update('Message', {'id': message_id}, {
|
|
362
362
|
'in_app_status': 'delivered',
|
|
363
363
|
'updated_at': datetime.now(timezone.utc),
|
|
364
364
|
})
|
|
@@ -375,38 +375,30 @@ class MessagingService:
|
|
|
375
375
|
preferences: Dict[str, Any],
|
|
376
376
|
) -> None:
|
|
377
377
|
"""Deliver email message."""
|
|
378
|
+
from .email_service import EmailMessage, EmailAddress, get_email_service
|
|
379
|
+
|
|
378
380
|
email = preferences.get('email_address')
|
|
379
|
-
|
|
380
381
|
if not email:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
# to=email,
|
|
397
|
-
# subject=subject,
|
|
398
|
-
# html=body_html,
|
|
399
|
-
# text=body_text,
|
|
400
|
-
# )
|
|
401
|
-
|
|
402
|
-
logger.info(f"📧 Email message queued: {message_id} -> {email}")
|
|
403
|
-
|
|
404
|
-
self.db.update('Message', message_id, {
|
|
405
|
-
'email_status': 'sent',
|
|
406
|
-
'email_sent_at': datetime.now(timezone.utc),
|
|
382
|
+
self.db.update('Message', {'id': message_id}, {'email_status': 'failed'})
|
|
383
|
+
raise ValueError("No email address configured")
|
|
384
|
+
service = self._email_provider or get_email_service()
|
|
385
|
+
message = EmailMessage(
|
|
386
|
+
to=[EmailAddress.parse(email)], message_id=message_id,
|
|
387
|
+
subject=rendered.get('email_subject', rendered.get('in_app_title', '')),
|
|
388
|
+
html=rendered.get('email_body_html') or None,
|
|
389
|
+
text=rendered.get('email_body_text', rendered.get('in_app_body', '')) or None,
|
|
390
|
+
)
|
|
391
|
+
result = await service.send_message(message, log_delivery=False)
|
|
392
|
+
accepted = result.success and result.provider != 'console'
|
|
393
|
+
self.db.update('Message', {'id': message_id}, {
|
|
394
|
+
'email_status': result.status.value if accepted else 'failed',
|
|
395
|
+
'email_message_id': result.provider_message_id,
|
|
396
|
+
'email_sent_at': result.sent_at if accepted else None,
|
|
407
397
|
'updated_at': datetime.now(timezone.utc),
|
|
408
398
|
})
|
|
409
|
-
|
|
399
|
+
if not accepted:
|
|
400
|
+
raise RuntimeError(result.error or "Email transport did not accept delivery")
|
|
401
|
+
|
|
410
402
|
async def _deliver_push(
|
|
411
403
|
self,
|
|
412
404
|
message_id: str,
|
|
@@ -418,7 +410,7 @@ class MessagingService:
|
|
|
418
410
|
|
|
419
411
|
if not subscription:
|
|
420
412
|
logger.warning(f"No push subscription for message {message_id}")
|
|
421
|
-
self.db.update('Message', message_id, {
|
|
413
|
+
self.db.update('Message', {'id': message_id}, {
|
|
422
414
|
'push_status': 'failed',
|
|
423
415
|
})
|
|
424
416
|
return
|
|
@@ -440,7 +432,7 @@ class MessagingService:
|
|
|
440
432
|
|
|
441
433
|
logger.info(f"🔔 Push message queued: {message_id}")
|
|
442
434
|
|
|
443
|
-
self.db.update('Message', message_id, {
|
|
435
|
+
self.db.update('Message', {'id': message_id}, {
|
|
444
436
|
'push_status': 'sent',
|
|
445
437
|
'push_sent_at': datetime.now(timezone.utc),
|
|
446
438
|
'updated_at': datetime.now(timezone.utc),
|
|
@@ -550,7 +542,7 @@ class MessagingService:
|
|
|
550
542
|
'provider': provider,
|
|
551
543
|
'error_message': error_message,
|
|
552
544
|
'attempted_at': datetime.now(timezone.utc),
|
|
553
|
-
'delivered_at': datetime.now(timezone.utc) if status == 'success' else None,
|
|
545
|
+
'delivered_at': datetime.now(timezone.utc) if status == 'success' and channel == 'in_app' else None,
|
|
554
546
|
'created_at': datetime.now(timezone.utc),
|
|
555
547
|
'updated_at': datetime.now(timezone.utc),
|
|
556
548
|
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Resend transactional transport. Broadcast campaigns use a separate contract."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from .email_service import EmailAddress, EmailProvider, EmailResult, EmailStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResendProvider(EmailProvider):
|
|
14
|
+
name = "resend"
|
|
15
|
+
|
|
16
|
+
def __init__(self, api_key=None):
|
|
17
|
+
self.api_key = api_key or os.getenv("RESEND_API_KEY")
|
|
18
|
+
|
|
19
|
+
def is_configured(self):
|
|
20
|
+
return bool(self.api_key)
|
|
21
|
+
|
|
22
|
+
async def send(self, message):
|
|
23
|
+
if not self.is_configured():
|
|
24
|
+
return EmailResult(False, message.message_id, status=EmailStatus.FAILED,
|
|
25
|
+
error="Resend API key not configured", provider=self.name)
|
|
26
|
+
sender = message.from_email or EmailAddress(os.getenv("EMAIL_FROM_ADDRESS", ""),
|
|
27
|
+
os.getenv("EMAIL_FROM_NAME", "OakQuant"))
|
|
28
|
+
if not sender.email:
|
|
29
|
+
return EmailResult(False, message.message_id, status=EmailStatus.FAILED,
|
|
30
|
+
error="Email sender not configured", provider=self.name)
|
|
31
|
+
payload = {"from": str(sender), "to": [str(r) for r in message.to],
|
|
32
|
+
"subject": message.subject}
|
|
33
|
+
for key in ("html", "text"):
|
|
34
|
+
if getattr(message, key):
|
|
35
|
+
payload[key] = getattr(message, key)
|
|
36
|
+
reply = message.reply_to or os.getenv("EMAIL_REPLY_TO")
|
|
37
|
+
if reply:
|
|
38
|
+
payload["reply_to"] = str(reply)
|
|
39
|
+
for key in ("cc", "bcc"):
|
|
40
|
+
if getattr(message, key):
|
|
41
|
+
payload[key] = [str(r) for r in getattr(message, key)]
|
|
42
|
+
if message.headers:
|
|
43
|
+
payload["headers"] = message.headers
|
|
44
|
+
if message.attachments:
|
|
45
|
+
payload["attachments"] = [dict(filename=a.filename,
|
|
46
|
+
content=base64.b64encode(a.content).decode(),
|
|
47
|
+
**({"content_id": a.content_id} if a.content_id else {}))
|
|
48
|
+
for a in message.attachments]
|
|
49
|
+
return await asyncio.to_thread(self._send, message.message_id, payload)
|
|
50
|
+
|
|
51
|
+
def _send(self, message_id, payload):
|
|
52
|
+
request = urllib.request.Request("https://api.resend.com/emails",
|
|
53
|
+
data=json.dumps(payload).encode(), method="POST", headers={
|
|
54
|
+
"Authorization": "Bearer " + self.api_key,
|
|
55
|
+
"Content-Type": "application/json", "User-Agent": "timber-common",
|
|
56
|
+
"Idempotency-Key": message_id})
|
|
57
|
+
try:
|
|
58
|
+
with urllib.request.urlopen(request, timeout=25) as response:
|
|
59
|
+
data = json.load(response)
|
|
60
|
+
if not data.get("id"):
|
|
61
|
+
raise ValueError("Missing provider message ID")
|
|
62
|
+
return EmailResult(True, message_id, provider_message_id=data["id"],
|
|
63
|
+
status=EmailStatus.QUEUED, provider=self.name,
|
|
64
|
+
sent_at=datetime.now(timezone.utc))
|
|
65
|
+
except urllib.error.HTTPError as exc:
|
|
66
|
+
error = f"Resend rejected request (HTTP {exc.code})"
|
|
67
|
+
except Exception:
|
|
68
|
+
# Provider bodies and exception strings may contain recipient data or credentials.
|
|
69
|
+
error = "Resend request failed; delivery acceptance unknown"
|
|
70
|
+
return EmailResult(False, message_id, status=EmailStatus.FAILED,
|
|
71
|
+
error=error, provider=self.name)
|
|
72
|
+
|
|
73
|
+
async def send_bulk(self, messages):
|
|
74
|
+
return [await self.send(message) for message in messages]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "timber-common"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.3"
|
|
4
4
|
description = "Configuration-driven persistence library with ml tools (finance related) config driven db model registration, llm model choice, automatic encryption, caching, vector search, and GDPR compliance for Python applications"
|
|
5
5
|
authors = ["Pumulo Sikaneta <pumulo@gmail.com>"]
|
|
6
6
|
maintainers = ["Pumulo Sikaneta <pumulo@gmail.com>"]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/decisioning/expression_engine.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
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/integrations/integration_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/inventory/available_capabilities.py
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/inventory/cached_capabilities.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
|
|
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
|
|
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
|
|
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
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/user_context_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/common/services/runtime_cache/workflow_catalog_cache.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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/01_model_design_patterns.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/02_service_architecture.md
RENAMED
|
File without changes
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/04_caching_strategies.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/best_practices/05_error_handling.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/01_system_architecture.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/02_config_driven_models.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/03_persistence_layer.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/04_vector_integration.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/design_guides/05_multi_app_support.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/04_financial_data_fetching.md
RENAMED
|
File without changes
|
{timber_common-1.2.2 → timber_common-1.2.3}/documentation/how_to/05_encryption_and_security.md
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|