blackant-sdk 1.0.2__py3-none-any.whl

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 (70) hide show
  1. blackant/__init__.py +31 -0
  2. blackant/auth/__init__.py +10 -0
  3. blackant/auth/blackant_auth.py +518 -0
  4. blackant/auth/keycloak_manager.py +363 -0
  5. blackant/auth/request_id.py +52 -0
  6. blackant/auth/role_assignment.py +443 -0
  7. blackant/auth/tokens.py +57 -0
  8. blackant/client.py +400 -0
  9. blackant/config/__init__.py +0 -0
  10. blackant/config/docker_config.py +457 -0
  11. blackant/config/keycloak_admin_config.py +107 -0
  12. blackant/docker/__init__.py +12 -0
  13. blackant/docker/builder.py +616 -0
  14. blackant/docker/client.py +983 -0
  15. blackant/docker/dao.py +462 -0
  16. blackant/docker/registry.py +172 -0
  17. blackant/exceptions.py +111 -0
  18. blackant/http/__init__.py +8 -0
  19. blackant/http/client.py +125 -0
  20. blackant/patterns/__init__.py +1 -0
  21. blackant/patterns/singleton.py +20 -0
  22. blackant/services/__init__.py +10 -0
  23. blackant/services/dao.py +414 -0
  24. blackant/services/registry.py +635 -0
  25. blackant/utils/__init__.py +8 -0
  26. blackant/utils/initialization.py +32 -0
  27. blackant/utils/logging.py +337 -0
  28. blackant/utils/request_id.py +13 -0
  29. blackant/utils/store.py +50 -0
  30. blackant_sdk-1.0.2.dist-info/METADATA +117 -0
  31. blackant_sdk-1.0.2.dist-info/RECORD +70 -0
  32. blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
  33. blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
  34. calculation/__init__.py +0 -0
  35. calculation/base.py +26 -0
  36. calculation/errors.py +2 -0
  37. calculation/impl/__init__.py +0 -0
  38. calculation/impl/my_calculation.py +144 -0
  39. calculation/impl/simple_calc.py +53 -0
  40. calculation/impl/test.py +1 -0
  41. calculation/impl/test_calc.py +36 -0
  42. calculation/loader.py +227 -0
  43. notifinations/__init__.py +8 -0
  44. notifinations/mail_sender.py +212 -0
  45. storage/__init__.py +0 -0
  46. storage/errors.py +10 -0
  47. storage/factory.py +26 -0
  48. storage/interface.py +19 -0
  49. storage/minio.py +106 -0
  50. task/__init__.py +0 -0
  51. task/dao.py +38 -0
  52. task/errors.py +10 -0
  53. task/log_adapter.py +11 -0
  54. task/parsers/__init__.py +0 -0
  55. task/parsers/base.py +13 -0
  56. task/parsers/callback.py +40 -0
  57. task/parsers/cmd_args.py +52 -0
  58. task/parsers/freetext.py +19 -0
  59. task/parsers/objects.py +50 -0
  60. task/parsers/request.py +56 -0
  61. task/resource.py +84 -0
  62. task/states/__init__.py +0 -0
  63. task/states/base.py +14 -0
  64. task/states/error.py +47 -0
  65. task/states/idle.py +12 -0
  66. task/states/ready.py +51 -0
  67. task/states/running.py +21 -0
  68. task/states/set_up.py +40 -0
  69. task/states/tear_down.py +29 -0
  70. task/task.py +358 -0
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MyCalculation - Default calculation implementation for BlackAnt SDK.
4
+
5
+ This class provides a production-ready wrapper around the simple_calc module,
6
+ implementing the CalculationBase interface required by the BlackAnt task system.
7
+ """
8
+
9
+ from typing import Dict, Any, Optional
10
+ import logging
11
+
12
+ from calculation.base import CalculationBase
13
+ from calculation.impl.simple_calc import calculate
14
+
15
+
16
+ class MyCalculation(CalculationBase):
17
+ """
18
+ Default calculation implementation using simple_calc logic.
19
+
20
+ This class implements the CalculationBase interface and provides
21
+ state machine compatibility (set_up → run → tear_down).
22
+
23
+ Attributes:
24
+ _logger: Logger instance for calculation events
25
+ _kwargs: Keyword arguments passed to calculation
26
+ _result: Calculation result (None until run() completes)
27
+ _is_running: Flag indicating calculation is in progress
28
+ """
29
+
30
+ def __init__(self, logger: logging.Logger, **kwargs: Any) -> None:
31
+ """
32
+ Initialize MyCalculation with logger and configuration.
33
+
34
+ Args:
35
+ logger: Logger instance for calculation events
36
+ **kwargs: Additional keyword arguments for calculation configuration
37
+ Expected keys:
38
+ - data: Dict with calculation input (default: kwargs itself)
39
+ - numbers: List of numbers to calculate (fallback)
40
+ """
41
+ super().__init__(logger)
42
+ self._kwargs = kwargs
43
+ self._result: Optional[Dict[str, Any]] = None
44
+ self._is_running = False
45
+
46
+ self._logger.info("MyCalculation instance created")
47
+ self._logger.debug(f"Configuration: {kwargs}")
48
+
49
+ def set_up(self) -> None:
50
+ """
51
+ Prepare calculation resources (state machine: idle → set_up).
52
+
53
+ This method is called by the task system before run().
54
+ Performs validation and resource initialization.
55
+ """
56
+ self._logger.info("MyCalculation setup started")
57
+
58
+ # Validate input data structure
59
+ data = self._kwargs.get('data', self._kwargs)
60
+ if not isinstance(data, dict):
61
+ self._logger.warning(f"Expected dict data, got {type(data).__name__}")
62
+
63
+ self._logger.info("MyCalculation setup completed successfully")
64
+
65
+ def run(self) -> None:
66
+ """
67
+ Execute the calculation (state machine: set_up → running).
68
+
69
+ This method performs the actual calculation using simple_calc.calculate().
70
+ Results are stored in self._result and accessible via the result property.
71
+ """
72
+ self._logger.info("MyCalculation execution started")
73
+ self._is_running = True
74
+
75
+ try:
76
+ # Extract data from kwargs (task sends data in 'data' key or root level)
77
+ data = self._kwargs.get('data', self._kwargs)
78
+
79
+ # Execute calculation using simple_calc function
80
+ self._logger.debug(f"Calling calculate() with data: {data}")
81
+ self._result = calculate(data)
82
+
83
+ self._logger.info(f"Calculation completed successfully: {self._result}")
84
+
85
+ except Exception as exc:
86
+ self._logger.error(f"Calculation failed: {exc}", exc_info=True)
87
+ self._result = {
88
+ 'error': str(exc),
89
+ 'status': 'failed'
90
+ }
91
+ raise
92
+
93
+ finally:
94
+ self._is_running = False
95
+
96
+ def tear_down(self) -> None:
97
+ """
98
+ Clean up calculation resources (state machine: running → tear_down).
99
+
100
+ This method is called by the task system after run() completes.
101
+ Performs cleanup and resource release.
102
+ """
103
+ self._logger.info("MyCalculation teardown started")
104
+
105
+ # Release resources (if any)
106
+ self._is_running = False
107
+
108
+ self._logger.info("MyCalculation teardown completed")
109
+
110
+ def stop(self) -> None:
111
+ """
112
+ Stop calculation execution (manual interrupt).
113
+
114
+ This method is called when the task is manually stopped.
115
+ Sets the running flag to False to signal interruption.
116
+ """
117
+ self._logger.warning("MyCalculation stop requested")
118
+ self._is_running = False
119
+
120
+ if self._result is None:
121
+ self._result = {
122
+ 'status': 'stopped',
123
+ 'message': 'Calculation was stopped before completion'
124
+ }
125
+
126
+ self._logger.info("MyCalculation stopped")
127
+
128
+ @property
129
+ def result(self) -> Optional[Dict[str, Any]]:
130
+ """
131
+ Get calculation result.
132
+
133
+ Returns:
134
+ Optional[Dict[str, Any]]: Calculation result or None if not yet executed
135
+ Expected structure:
136
+ {
137
+ 'result': int/float, # Sum of numbers
138
+ 'count': int, # Number count
139
+ 'average': float, # Average value
140
+ 'timestamp': str, # ISO format timestamp
141
+ 'service_name': str # Service identifier
142
+ }
143
+ """
144
+ return self._result
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """Simple calculation implementation for BlackAnt SDK build_service testing."""
3
+
4
+ import json
5
+ import time
6
+ import os
7
+ from datetime import datetime
8
+
9
+
10
+ def calculate(data):
11
+ """Simple calculation function.
12
+
13
+ Args:
14
+ data: Input data dictionary with 'numbers' list
15
+
16
+ Returns:
17
+ dict: Calculation result
18
+ """
19
+ numbers = data.get('numbers', [])
20
+
21
+ # Simple calculation: sum of numbers
22
+ result = sum(numbers)
23
+
24
+ # Add some processing time to simulate real calculation
25
+ time.sleep(0.5)
26
+
27
+ return {
28
+ 'result': result,
29
+ 'count': len(numbers),
30
+ 'average': result / len(numbers) if numbers else 0,
31
+ 'timestamp': datetime.now().isoformat(),
32
+ 'service_name': os.getenv('SERVICE_NAME', 'simple-calc')
33
+ }
34
+
35
+
36
+ def main():
37
+ """Main function for standalone execution."""
38
+ # Test data
39
+ test_data = {
40
+ 'numbers': [1, 2, 3, 4, 5, 10, 15, 20]
41
+ }
42
+
43
+ print("🧮 BlackAnt Calculation Service Starting...")
44
+ print(f"📊 Input data: {test_data}")
45
+
46
+ result = calculate(test_data)
47
+
48
+ print(f"✅ Calculation completed: {json.dumps(result, indent=2)}")
49
+ print("🎯 Service ready for BlackAnt deployment!")
50
+
51
+
52
+ if __name__ == '__main__':
53
+ main()
@@ -0,0 +1 @@
1
+ print('test')
@@ -0,0 +1,36 @@
1
+ """Test calculation module for BlackAnt SDK live test."""
2
+
3
+ from calculation.base import BaseCalculation
4
+
5
+
6
+ class TestCalculation(BaseCalculation):
7
+ """Simple test calculation for SDK verification."""
8
+
9
+ def calculate(self, input_data: dict) -> dict:
10
+ """
11
+ Simple calculation that processes a list of numbers.
12
+
13
+ Args:
14
+ input_data: dict with 'numbers' key containing list of numbers
15
+
16
+ Returns:
17
+ dict with sum, average, min, max
18
+ """
19
+ numbers = input_data.get('numbers', [])
20
+
21
+ if not numbers:
22
+ return {
23
+ 'error': 'No numbers provided',
24
+ 'status': 'failed'
25
+ }
26
+
27
+ result = {
28
+ 'sum': sum(numbers),
29
+ 'average': sum(numbers) / len(numbers),
30
+ 'min': min(numbers),
31
+ 'max': max(numbers),
32
+ 'count': len(numbers),
33
+ 'status': 'success'
34
+ }
35
+
36
+ return result
calculation/loader.py ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CalculationLoader - Dynamic calculation loading system for BlackAnt SDK.
4
+
5
+ This module provides thread-safe, singleton-based dynamic loading of calculation
6
+ implementations. Supports environment variable configuration and automatic fallback.
7
+
8
+ Designed for production use with Gunicorn multi-worker environments (8 workers).
9
+ """
10
+
11
+ import os
12
+ import threading
13
+ import importlib
14
+ from typing import Type, Dict, Optional
15
+
16
+ from calculation.base import CalculationBase
17
+ from blackant.utils.logging import get_logger
18
+
19
+
20
+ class CalculationLoader:
21
+ """
22
+ Thread-safe singleton loader for dynamic calculation instantiation.
23
+
24
+ This loader supports:
25
+ - Environment variable configuration (CALCULATION_MODULE, CALCULATION_CLASS)
26
+ - Thread-safe caching for multi-worker environments
27
+ - Automatic fallback to MyCalculation
28
+ - Type validation (must inherit from CalculationBase)
29
+
30
+ Thread Safety:
31
+ Designed for Gunicorn with --workers=8 configuration.
32
+ Uses threading.Lock for cache synchronization across threads.
33
+
34
+ Usage:
35
+ >>> loader = CalculationLoader()
36
+ >>> CalculationClass = loader.get_calculation()
37
+ >>> calc = CalculationClass(logger, **kwargs)
38
+ """
39
+
40
+ # Class-level singleton instance
41
+ _instance: Optional['CalculationLoader'] = None
42
+ _instance_lock = threading.Lock()
43
+
44
+ def __new__(cls) -> 'CalculationLoader':
45
+ """
46
+ Create or return singleton instance (thread-safe).
47
+
48
+ Returns:
49
+ CalculationLoader: Singleton instance
50
+ """
51
+ if not cls._instance:
52
+ with cls._instance_lock:
53
+ # Double-check locking pattern for thread safety
54
+ if not cls._instance:
55
+ cls._instance = super().__new__(cls)
56
+ cls._instance._initialize()
57
+ return cls._instance
58
+
59
+ def _initialize(self) -> None:
60
+ """Initialize loader state (called once by __new__)."""
61
+ self._cache: Dict[str, Type[CalculationBase]] = {}
62
+ self._cache_lock = threading.Lock()
63
+ self._logger = get_logger("CalculationLoader")
64
+
65
+ self._logger.info("CalculationLoader singleton initialized")
66
+
67
+ def get_calculation(self) -> Type[CalculationBase]:
68
+ """
69
+ Get calculation class based on environment configuration.
70
+
71
+ Environment Variables:
72
+ CALCULATION_MODULE: Python module path (default: calculation.impl.my_calculation)
73
+ CALCULATION_CLASS: Class name (default: MyCalculation)
74
+
75
+ Returns:
76
+ Type[CalculationBase]: Calculation class ready for instantiation
77
+
78
+ Raises:
79
+ TypeError: If loaded class does not inherit from CalculationBase
80
+
81
+ Example:
82
+ >>> os.environ['CALCULATION_MODULE'] = 'calculation.impl.simple_calc'
83
+ >>> os.environ['CALCULATION_CLASS'] = 'SimpleCalculation'
84
+ >>> loader = CalculationLoader()
85
+ >>> CalcClass = loader.get_calculation()
86
+ >>> calc = CalcClass(logger)
87
+ """
88
+ # Get configuration from environment
89
+ module_path = os.getenv('CALCULATION_MODULE', 'calculation.impl.my_calculation')
90
+ class_name = os.getenv('CALCULATION_CLASS', 'MyCalculation')
91
+
92
+ cache_key = f"{module_path}.{class_name}"
93
+
94
+ # Check cache first (thread-safe read)
95
+ with self._cache_lock:
96
+ if cache_key in self._cache:
97
+ self._logger.debug(f"Cache hit: {cache_key}")
98
+ return self._cache[cache_key]
99
+
100
+ # Cache miss - load calculation
101
+ self._logger.info(f"Loading calculation: {module_path}.{class_name}")
102
+
103
+ try:
104
+ calc_class = self._load_calculation(module_path, class_name)
105
+
106
+ # Cache the result (thread-safe write)
107
+ with self._cache_lock:
108
+ self._cache[cache_key] = calc_class
109
+
110
+ self._logger.info(f"Successfully loaded and cached: {cache_key}")
111
+ return calc_class
112
+
113
+ except Exception as exc:
114
+ self._logger.error(f"Failed to load {cache_key}: {exc}")
115
+ self._logger.warning("Falling back to default MyCalculation")
116
+
117
+ # Fallback to default MyCalculation
118
+ return self._load_fallback()
119
+
120
+ def _load_calculation(self, module_path: str, class_name: str) -> Type[CalculationBase]:
121
+ """
122
+ Load calculation class from module path.
123
+
124
+ Args:
125
+ module_path: Python module path (e.g., 'calculation.impl.my_calculation')
126
+ class_name: Class name (e.g., 'MyCalculation')
127
+
128
+ Returns:
129
+ Type[CalculationBase]: Loaded calculation class
130
+
131
+ Raises:
132
+ ImportError: If module cannot be imported
133
+ AttributeError: If class does not exist in module
134
+ TypeError: If class is not a CalculationBase subclass
135
+ """
136
+ # Import module
137
+ try:
138
+ module = importlib.import_module(module_path)
139
+ except ImportError as exc:
140
+ self._logger.error(f"Cannot import module '{module_path}': {exc}")
141
+ raise
142
+
143
+ # Get class from module
144
+ try:
145
+ calc_class = getattr(module, class_name)
146
+ except AttributeError as exc:
147
+ self._logger.error(f"Class '{class_name}' not found in '{module_path}': {exc}")
148
+ raise
149
+
150
+ # Validate it's a CalculationBase subclass
151
+ if not isinstance(calc_class, type):
152
+ raise TypeError(f"{class_name} is not a class (got {type(calc_class).__name__})")
153
+
154
+ if not issubclass(calc_class, CalculationBase):
155
+ raise TypeError(
156
+ f"{class_name} must inherit from CalculationBase "
157
+ f"(current bases: {[b.__name__ for b in calc_class.__bases__]})"
158
+ )
159
+
160
+ self._logger.debug(f"Validated: {class_name} is a valid CalculationBase subclass")
161
+ return calc_class
162
+
163
+ def _load_fallback(self) -> Type[CalculationBase]:
164
+ """
165
+ Load fallback calculation (MyCalculation).
166
+
167
+ Returns:
168
+ Type[CalculationBase]: MyCalculation class
169
+
170
+ Raises:
171
+ ImportError: If even fallback cannot be loaded (critical error)
172
+ """
173
+ fallback_module = 'calculation.impl.my_calculation'
174
+ fallback_class = 'MyCalculation'
175
+
176
+ cache_key = f"{fallback_module}.{fallback_class}"
177
+
178
+ # Check if fallback is already cached
179
+ with self._cache_lock:
180
+ if cache_key in self._cache:
181
+ return self._cache[cache_key]
182
+
183
+ try:
184
+ from calculation.impl.my_calculation import MyCalculation
185
+
186
+ # Cache fallback
187
+ with self._cache_lock:
188
+ self._cache[cache_key] = MyCalculation
189
+
190
+ self._logger.info("Fallback MyCalculation loaded successfully")
191
+ return MyCalculation
192
+
193
+ except ImportError as exc:
194
+ self._logger.critical(f"CRITICAL: Cannot load fallback MyCalculation: {exc}")
195
+ raise ImportError(
196
+ "CalculationLoader failed: cannot load configured calculation "
197
+ "and fallback MyCalculation is also unavailable. "
198
+ "Ensure calculation/impl/my_calculation.py exists."
199
+ ) from exc
200
+
201
+ def clear_cache(self) -> None:
202
+ """
203
+ Clear calculation cache (useful for testing or hot-reload).
204
+
205
+ Thread-safe operation.
206
+ """
207
+ with self._cache_lock:
208
+ cache_size = len(self._cache)
209
+ self._cache.clear()
210
+ self._logger.info(f"Cache cleared ({cache_size} entries removed)")
211
+
212
+ def get_cache_info(self) -> Dict[str, int]:
213
+ """
214
+ Get cache statistics.
215
+
216
+ Returns:
217
+ Dict[str, int]: Cache information
218
+ {
219
+ 'size': Number of cached calculations,
220
+ 'entries': List of cached keys
221
+ }
222
+ """
223
+ with self._cache_lock:
224
+ return {
225
+ 'size': len(self._cache),
226
+ 'entries': list(self._cache.keys())
227
+ }
@@ -0,0 +1,8 @@
1
+ """Notifications module for BlackAnt SDK.
2
+
3
+ This module provides email sending functionality.
4
+ """
5
+
6
+ from notifinations.mail_sender import EmailSender
7
+
8
+ __all__ = ["EmailSender"]
@@ -0,0 +1,212 @@
1
+ """Email sender module.
2
+
3
+ Module for sending email messages via SMTP.
4
+
5
+ Example usage:
6
+ from mail_sender import EmailSender
7
+ mail_sender_inst = EmailSender()
8
+ mail_sender_inst.send_mail(
9
+ subject="My subject",
10
+ mail_body="My body",
11
+ addr_to="who_get_mail@mail.com",
12
+ addr_from="no_reply@blackant.app",
13
+ addr_cc="get_copy_mail@mail.com",
14
+ attachments=["file1.txt", "file2.json"],
15
+ reply_to="personal_mail@mail.com"
16
+ )
17
+ """
18
+
19
+ import os
20
+ import re
21
+ import smtplib
22
+ from email.mime.application import MIMEApplication
23
+ from email.mime.multipart import MIMEMultipart
24
+ from email.mime.text import MIMEText
25
+ from logging import Logger
26
+ from typing import List, Optional, Union
27
+
28
+ from blackant.utils.logging import get_logger
29
+
30
+
31
+ class EmailSender: # pylint: disable=too-few-public-methods
32
+ """Email sender class for SMTP operations.
33
+
34
+ This class handles email sending operations with support for
35
+ attachments, CC recipients, and reply-to addresses.
36
+
37
+ The init method creates an SMTP client and handles authentication.
38
+ Emails are constructed using the email.mime module and sent to recipients.
39
+ """
40
+
41
+ def __init__(self, logger: Optional[Logger] = None) -> None:
42
+ """Initialize the logger object and SMTP client.
43
+
44
+ Args:
45
+ logger: External logger to use. If None, creates default logger.
46
+
47
+ Environment variables:
48
+ SMTP_HOST: SMTP server hostname (default: mail.blackant.app)
49
+ SMTP_PORT: SMTP server port (default: 26)
50
+ SMTP_USER: SMTP username (required)
51
+ SMTP_PASSWORD: SMTP password (required)
52
+ """
53
+ self.__logger = logger or get_logger("email_sender")
54
+
55
+ # Get SMTP configuration from environment
56
+ smtp_host = os.getenv("SMTP_HOST", "mail.blackant.app")
57
+ smtp_port = int(os.getenv("SMTP_PORT", "26"))
58
+ smtp_user = os.getenv("SMTP_USER")
59
+ smtp_password = os.getenv("SMTP_PASSWORD")
60
+
61
+ if not smtp_user or not smtp_password:
62
+ raise ValueError(
63
+ "SMTP_USER and SMTP_PASSWORD environment variables are required. "
64
+ "Please set them before using EmailSender."
65
+ )
66
+
67
+ self.__logger.info("Initialize the SMTP client")
68
+ try:
69
+ self.__smtp_client = smtplib.SMTP(host=smtp_host, port=smtp_port)
70
+ except Exception:
71
+ self.__logger.exception("Cannot create the SMTP client")
72
+ raise
73
+ self.__logger.info("The SMTP client has been initialized")
74
+ try:
75
+ self.__smtp_client.login(user=smtp_user, password=smtp_password)
76
+ except Exception:
77
+ self.__logger.exception("Cannot login to SMTP server")
78
+ raise
79
+ self.__logger.info("Login to SMTP server has been successful")
80
+
81
+ def __string_to_list(self, string_param: Union[str, List, tuple, None]) -> List[str]:
82
+ """Convert input to list type.
83
+
84
+ Attempts to convert the input argument to list type.
85
+ Handles most built-in types that can be cast to list.
86
+
87
+ Args:
88
+ string_param: The object to convert, typically a string.
89
+ Can also handle other types (e.g., Tuple).
90
+
91
+ Returns:
92
+ list: The converted list.
93
+ """
94
+
95
+ list_to_return: list = []
96
+ if not string_param:
97
+ return list_to_return
98
+ if isinstance(string_param, str):
99
+ list_to_return.extend(re.split(r"[,;]", string_param))
100
+ return list_to_return
101
+ try:
102
+ return list(string_param)
103
+ except TypeError as type_error:
104
+ self.__logger.exception("Cannot convert the string to list.")
105
+ raise type_error
106
+
107
+ def send_mail( # pylint: disable=too-many-arguments
108
+ self,
109
+ subject: str,
110
+ mail_body: str,
111
+ addr_to: Union[str, List[str]],
112
+ addr_from: str = "no-reply@blackant.app",
113
+ addr_cc: Optional[Union[str, List[str]]] = None,
114
+ attachments: Optional[List[str]] = None,
115
+ reply_to: Optional[str] = None,
116
+ ) -> None:
117
+ """Send an email message.
118
+
119
+ Constructs and sends an email based on the provided arguments.
120
+
121
+ Args:
122
+ subject: Email subject line.
123
+ mail_body: Email content (Text/HTML).
124
+ addr_to: Recipient email address.
125
+ addr_from: Sender email address. Defaults to no-reply@blackant.app.
126
+ addr_cc: Carbon copy recipient email address.
127
+ attachments: List of file paths to attach.
128
+ reply_to: Email address to use for replies.
129
+ """
130
+
131
+ addr_to = self.__string_to_list(addr_to)
132
+ addr_cc = self.__string_to_list(addr_cc)
133
+
134
+ if attachments:
135
+ if not isinstance(attachments, list):
136
+ raise TypeError("Attachments has to be list type.")
137
+ if all(single_attachment is None for single_attachment in attachments):
138
+ self.__logger.warning(
139
+ "All elements of attachment list are None. Nothing will be attached to mail."
140
+ )
141
+ attachments = []
142
+ try:
143
+ self.__logger.info("E-mail will be sent to {}".format(addr_to))
144
+ if addr_cc:
145
+ self.__logger.info("Copy will be sent to {}".format(addr_cc))
146
+ message = self.__get_message_body(
147
+ subject=subject,
148
+ mail_body=mail_body,
149
+ addr_to=addr_to,
150
+ addr_from=addr_from,
151
+ addr_cc=addr_cc,
152
+ attachments=attachments,
153
+ reply_to=reply_to,
154
+ )
155
+
156
+ self.__logger.debug("The created message: {}".format(message.as_string()))
157
+
158
+ addr_to.extend(addr_cc)
159
+ self.__logger.info("Starting to send out the E-mail.")
160
+ self.__smtp_client.sendmail(addr_from, addr_to, message.as_string())
161
+ self.__logger.info("The E-mail has been sent out successfully.")
162
+ except smtplib.SMTPConnectError:
163
+ self.__logger.exception("Cannot connect to SMTP server.")
164
+ except smtplib.SMTPDataError:
165
+ self.__logger.exception("Data error in mail sending.")
166
+ finally:
167
+ self.__logger.info("Closing the SMTP connection.")
168
+ self.__smtp_client.quit()
169
+
170
+ @staticmethod
171
+ def __get_message_body( # pylint: disable=too-many-arguments
172
+ subject: str,
173
+ mail_body: str,
174
+ addr_to: List[str],
175
+ addr_from: str,
176
+ addr_cc: List[str],
177
+ attachments: Optional[List[str]] = None,
178
+ reply_to: Optional[str] = None
179
+ ) -> MIMEMultipart:
180
+ """Construct the email message body.
181
+
182
+ Creates the email content based on the provided arguments.
183
+
184
+ Args:
185
+ subject: Email subject line.
186
+ mail_body: Email content (Text/HTML).
187
+ addr_to: Recipient email address.
188
+ addr_from: Sender email address. Defaults to no-reply@blackant.app.
189
+ addr_cc: Carbon copy recipient email address.
190
+ attachments: List of file paths to attach.
191
+ reply_to: Email address to use for replies.
192
+
193
+ Returns:
194
+ MIMEMultipart: The constructed email message.
195
+ """
196
+
197
+ msg = MIMEMultipart("alternative")
198
+ msg["To"] = ", ".join(addr_to) if addr_to else ""
199
+ msg["CC"] = ", ".join(addr_cc) if addr_cc else ""
200
+ msg["From"] = addr_from
201
+ msg["Subject"] = subject
202
+ msg.attach(MIMEText(mail_body, "html"))
203
+ if reply_to:
204
+ msg.add_header("reply-to", reply_to)
205
+ for single_file in attachments or []:
206
+ with open(single_file, "rb") as opened_file:
207
+ part = MIMEApplication(opened_file.read(), Name=os.path.basename(single_file))
208
+ part["Content-Disposition"] = 'attachment; filename="{}"'.format(
209
+ os.path.basename(single_file)
210
+ )
211
+ msg.attach(part)
212
+ return msg
storage/__init__.py ADDED
File without changes