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
blackant/exceptions.py ADDED
@@ -0,0 +1,111 @@
1
+ """BlackAnt SDK exception classes.
2
+
3
+ Central exception hierarchy for all BlackAnt SDK errors.
4
+ """
5
+
6
+
7
+ class BlackAntException(Exception):
8
+ """Base exception for all BlackAnt SDK errors.
9
+
10
+ All SDK-specific exceptions inherit from this base class
11
+ to allow catching all SDK errors with a single handler.
12
+ """
13
+
14
+
15
+ class BlackAntAuthenticationError(BlackAntException):
16
+ """Authentication and authorization related errors.
17
+
18
+ Raised when:
19
+ - Login credentials are invalid
20
+ - Keycloak authentication fails
21
+ - Bearer token is missing or expired
22
+ - Authorization is denied
23
+ """
24
+
25
+
26
+ class BlackAntDockerError(BlackAntException):
27
+ """Docker operation related errors.
28
+
29
+ Raised when:
30
+ - Docker API calls fail
31
+ - Container operations encounter issues
32
+ - Image operations fail
33
+ - Registry communication errors
34
+ """
35
+
36
+
37
+ class BlackAntConnectionError(BlackAntException):
38
+ """Network and connection related errors.
39
+
40
+ Raised when:
41
+ - HTTP requests timeout
42
+ - Network connection fails
43
+ - API endpoints are unreachable
44
+ """
45
+
46
+
47
+ class BlackAntConfigurationError(BlackAntException):
48
+ """Configuration related errors.
49
+
50
+ Raised when:
51
+ - Required environment variables are missing
52
+ - Configuration files are invalid
53
+ - Service configuration is incorrect
54
+ """
55
+
56
+
57
+ # =============================================================================
58
+ # Keycloak & Role Assignment Exceptions
59
+ # =============================================================================
60
+
61
+
62
+ class KeycloakError(BlackAntException):
63
+ """Base exception for Keycloak-related errors.
64
+
65
+ All Keycloak-specific exceptions inherit from this base class
66
+ to allow catching all Keycloak errors with a single handler.
67
+ """
68
+
69
+
70
+ class KeycloakConnectionError(KeycloakError):
71
+ """Raised when connection to Keycloak server fails.
72
+
73
+ This can occur due to:
74
+ - Network connectivity issues
75
+ - Incorrect server URL
76
+ - Firewall blocking connection
77
+ - Keycloak server being down
78
+ """
79
+
80
+
81
+ class KeycloakAuthenticationError(KeycloakError):
82
+ """Raised when Keycloak authentication fails.
83
+
84
+ This can occur due to:
85
+ - Invalid service account credentials
86
+ - Expired client secret
87
+ - Incorrect client ID
88
+ - Insufficient permissions for service account
89
+ """
90
+
91
+
92
+ class RoleAssignmentError(KeycloakError):
93
+ """Raised when role assignment operation fails.
94
+
95
+ This can occur due to:
96
+ - Role does not exist in realm
97
+ - User does not exist
98
+ - Insufficient permissions to assign role
99
+ - Keycloak API error during assignment
100
+ """
101
+
102
+
103
+ class TokenValidationError(BlackAntException):
104
+ """Raised when JWT token validation fails.
105
+
106
+ This can occur due to:
107
+ - Invalid token format
108
+ - Missing required claims (sub, etc.)
109
+ - Expired token
110
+ - Token signature validation failure
111
+ """
@@ -0,0 +1,8 @@
1
+ """BlackAnt SDK HTTP module.
2
+
3
+ Provides HTTP client functionality for BlackAnt SDK API communication.
4
+ """
5
+
6
+ from .client import HTTPClient, HTTPConnectionError
7
+
8
+ __all__ = ["HTTPClient", "HTTPConnectionError"]
@@ -0,0 +1,125 @@
1
+ """BlackAnt SDK HTTP communication module.
2
+
3
+ HTTP client with Bearer token authentication, request ID tracking,
4
+ and integration with auth store modules.
5
+ """
6
+
7
+ import requests
8
+ from requests.adapters import HTTPAdapter
9
+
10
+ from ..auth.tokens import AuthTokenStore
11
+ from ..auth.request_id import RequestIdStore
12
+ from ..utils.logging import get_logger, set_request_id, clear_request_id
13
+
14
+
15
+ class HTTPConnectionError(Exception):
16
+ """HTTP connection specific exception.
17
+
18
+ Raised when HTTP requests fail due to connection issues,
19
+ timeouts, or network problems.
20
+ """
21
+
22
+
23
+ class HTTPClient: # pylint: disable=too-few-public-methods
24
+ """HTTP client for BlackAnt SDK API communication.
25
+
26
+ Provides HTTP request functionality with automatic Bearer token injection,
27
+ request ID tracking, and base URL management.
28
+
29
+ Args:
30
+ base_url (str): Base URL for API endpoints.
31
+ auth_token_store (AuthTokenStore, optional): Token storage instance.
32
+ request_id_store (RequestIdStore, optional): Request ID storage instance.
33
+
34
+ Examples:
35
+ >>> client = HTTPClient("https://api.blackant.app")
36
+ >>> client.auth_token_store.user_token = "your_token"
37
+ >>> response = client.send_request("/api/v1/services", "GET")
38
+ """
39
+
40
+ def __init__(self, base_url, auth_token_store=None, request_id_store=None):
41
+ """Initialize HTTP client.
42
+
43
+ Args:
44
+ base_url (str): Base URL for API endpoints.
45
+ auth_token_store (AuthTokenStore, optional): Token storage.
46
+ request_id_store (RequestIdStore, optional): Request ID storage.
47
+ """
48
+ self.base_url = base_url.rstrip("/")
49
+ self.auth_token_store = auth_token_store or AuthTokenStore()
50
+ self.request_id_store = request_id_store or RequestIdStore()
51
+ self.logger = get_logger("http")
52
+
53
+ self.session = requests.Session()
54
+ adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20, max_retries=0)
55
+ self.session.mount("https://", adapter)
56
+ self.session.mount("http://", adapter)
57
+
58
+ def send_request( # pylint: disable=too-many-arguments
59
+ self,
60
+ endpoint,
61
+ method,
62
+ json=None,
63
+ anonymous=False,
64
+ is_admin=False,
65
+ req_timeout=60.0,
66
+ token=None,
67
+ ):
68
+ """Send HTTP request to the configured API endpoint.
69
+
70
+ Args:
71
+ endpoint (str): API endpoint path.
72
+ method (str): HTTP method.
73
+ json (dict, optional): JSON data for request body.
74
+ anonymous (bool): Skip Bearer token authentication.
75
+ is_admin (bool): Use admin token instead of user token.
76
+ req_timeout (float): Request timeout in seconds.
77
+ token (str, optional): Custom Bearer token.
78
+
79
+ Returns:
80
+ requests.Response: HTTP response object.
81
+
82
+ Raises:
83
+ HTTPConnectionError: When connection fails or times out.
84
+ """
85
+ request_id = self.request_id_store.generate_id()
86
+ set_request_id(request_id)
87
+
88
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
89
+
90
+ try:
91
+ req = requests.Request(method, url, json=json)
92
+ prepped = self.session.prepare_request(req)
93
+
94
+ if not anonymous:
95
+ if token:
96
+ prepped.headers["Authorization"] = f"Bearer {token}"
97
+ else:
98
+ auth_token = (
99
+ self.auth_token_store.admin_token if is_admin
100
+ else self.auth_token_store.user_token
101
+ )
102
+ if auth_token:
103
+ prepped.headers["Authorization"] = f"Bearer {auth_token}"
104
+
105
+ prepped.headers[self.request_id_store.header_name] = request_id
106
+
107
+ # Determine if we should verify SSL (skip for localhost/dev)
108
+ verify_ssl = not (
109
+ "localhost" in self.base_url or
110
+ "127.0.0.1" in self.base_url or
111
+ "http://" in url or # HTTP doesn't need SSL verification
112
+ "dev.blackant.app" in self.base_url # Dev environment self-signed cert
113
+ )
114
+
115
+ response = self.session.send(prepped, timeout=req_timeout, verify=verify_ssl)
116
+ return response
117
+
118
+ except requests.exceptions.ConnectionError as exc:
119
+ self.logger.error(f"Connection error: {exc}")
120
+ raise HTTPConnectionError from exc
121
+ except requests.exceptions.Timeout as exc:
122
+ self.logger.error(f"Request time-out error: {exc}")
123
+ raise HTTPConnectionError from exc
124
+ finally:
125
+ clear_request_id()
@@ -0,0 +1 @@
1
+ """Pattern implementations for BlackAnt SDK."""
@@ -0,0 +1,20 @@
1
+ """Singleton pattern implementation for BlackAnt SDK."""
2
+
3
+ import threading
4
+
5
+
6
+ class Singleton(type):
7
+ """Singleton metaclass implementation.
8
+
9
+ Thread-safe singleton pattern using metaclass approach.
10
+ Ensures only one instance per class exists across the application.
11
+ """
12
+ _instances = {}
13
+ _lock = threading.Lock()
14
+
15
+ def __call__(cls, *args, **kwargs):
16
+ if cls not in cls._instances:
17
+ with cls._lock:
18
+ if cls not in cls._instances:
19
+ cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
20
+ return cls._instances[cls]
@@ -0,0 +1,10 @@
1
+ """BlackAnt Services module.
2
+
3
+ Service registry and lifecycle management components with both
4
+ HTTP API (external) and Docker SDK (internal) support.
5
+ """
6
+
7
+ from .registry import ServiceRegistry
8
+ from .dao import ServiceManagerDAO
9
+
10
+ __all__ = ["ServiceRegistry", "ServiceManagerDAO"]
@@ -0,0 +1,414 @@
1
+ """ServiceManager DAO - Docker SDK objektum szintű service kezelés.
2
+
3
+ High-level Docker service operations using Docker SDK objects directly,
4
+ following the BlackAnt "Docker as Object" design pattern.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ import random
10
+ from typing import Optional, List, Dict, Any, Union
11
+ from dataclasses import dataclass, field
12
+
13
+ import docker
14
+ from docker.models.services import Service as DockerServiceObject
15
+ from docker.models.containers import Container as DockerContainer
16
+ from docker.errors import DockerException, NotFound, APIError
17
+
18
+ from ..utils.logging import get_logger
19
+ from ..exceptions import BlackAntDockerError
20
+
21
+
22
+ @dataclass
23
+ class ServiceConfig:
24
+ """Service configuration data class for ServiceManagerDAO."""
25
+
26
+ name: str = field(default="")
27
+ image: str = field(default="")
28
+ command: Optional[List[str]] = field(default=None)
29
+ environments: Dict[str, str] = field(default_factory=dict)
30
+ networks: List[str] = field(default_factory=list)
31
+ labels: Dict[str, str] = field(default_factory=dict)
32
+ constraints: List[str] = field(default_factory=list)
33
+ replicas: int = field(default=1)
34
+ cpu_limit: Optional[int] = field(default=None) # nanocpus
35
+ memory_limit: Optional[int] = field(default=None) # bytes
36
+ ports: Dict[str, int] = field(default_factory=dict)
37
+ mounts: List[Dict[str, str]] = field(default_factory=list)
38
+
39
+
40
+ class ServiceManagerDAO:
41
+ """Docker Services objektum szintű kezelése.
42
+
43
+ Provides high-level service management operations using Docker SDK
44
+ objects directly instead of HTTP requests. This follows the BlackAnt
45
+ "Docker as Object" pattern for internal server-side operations.
46
+
47
+ Examples:
48
+ >>> dao = ServiceManagerDAO()
49
+ >>> config = ServiceConfig(name="my-service", image="nginx:latest")
50
+ >>> service = dao.create_service(config)
51
+ >>> services = dao.list_services()
52
+ >>> dao.remove_service(service.id)
53
+ """
54
+
55
+ def __init__(self, docker_host: Optional[str] = None):
56
+ """ServiceManager DAO inicializálása Docker client-tel.
57
+
58
+ Args:
59
+ docker_host: Optional Docker daemon host URL
60
+
61
+ Raises:
62
+ BlackAntDockerError: Ha a Docker client inicializálás sikertelen
63
+ """
64
+ self.logger = get_logger("service-manager-dao")
65
+
66
+ try:
67
+ if docker_host:
68
+ self.__docker_client = docker.DockerClient(base_url=docker_host)
69
+ else:
70
+ self.__docker_client = docker.from_env()
71
+
72
+ # Test connection
73
+ self.__docker_client.ping()
74
+ self.logger.info("ServiceManagerDAO initialized successfully")
75
+
76
+ except DockerException as docker_error:
77
+ error_msg = f"Docker client initialization failed: {docker_error}"
78
+ self.logger.error(error_msg)
79
+ raise BlackAntDockerError(error_msg) from docker_error
80
+
81
+ def create_service(self, service_config: Union[ServiceConfig, Dict[str, Any]]) -> DockerServiceObject:
82
+ """Szolgáltatás létrehozása Docker SDK-val.
83
+
84
+ Args:
85
+ service_config: ServiceConfig object or configuration dictionary
86
+
87
+ Returns:
88
+ DockerServiceObject: Létrehozott Docker service objektum
89
+
90
+ Raises:
91
+ BlackAntDockerError: Docker API hiba esetén
92
+ """
93
+ # Convert dict to ServiceConfig if needed
94
+ if isinstance(service_config, dict):
95
+ config = ServiceConfig(**service_config)
96
+ else:
97
+ config = service_config
98
+
99
+ try:
100
+ # Prepare service creation parameters
101
+ create_params = {
102
+ 'name': config.name,
103
+ 'image': config.image,
104
+ 'env': [f"{k}={v}" for k, v in config.environments.items()],
105
+ 'networks': config.networks,
106
+ 'labels': config.labels,
107
+ 'mode': docker.types.ServiceMode('replicated', config.replicas)
108
+ }
109
+
110
+ # Add command if specified
111
+ if config.command:
112
+ create_params['command'] = config.command
113
+
114
+ # Add resource constraints if specified
115
+ if config.cpu_limit or config.memory_limit:
116
+ resources = docker.types.Resources(
117
+ cpu_limit=config.cpu_limit,
118
+ mem_limit=config.memory_limit
119
+ )
120
+ create_params['resources'] = resources
121
+
122
+ # Add placement constraints
123
+ if config.constraints:
124
+ create_params['constraints'] = config.constraints
125
+
126
+ # Add port mappings if specified
127
+ if config.ports:
128
+ endpoint_spec = docker.types.EndpointSpec(
129
+ ports={port: target for port, target in config.ports.items()}
130
+ )
131
+ create_params['endpoint_spec'] = endpoint_spec
132
+
133
+ # Add mounts if specified
134
+ if config.mounts:
135
+ mounts = []
136
+ for mount_config in config.mounts:
137
+ mount = docker.types.Mount(
138
+ target=mount_config['target'],
139
+ source=mount_config['source'],
140
+ type=mount_config.get('type', 'bind'),
141
+ read_only=mount_config.get('read_only', False)
142
+ )
143
+ mounts.append(mount)
144
+ create_params['mounts'] = mounts
145
+
146
+ # Create service using Docker SDK
147
+ service = self.__docker_client.services.create(**create_params)
148
+
149
+ self.logger.info(f"Service created successfully: {service.name} (ID: {service.id})")
150
+ return service
151
+
152
+ except APIError as api_error:
153
+ error_msg = f"Service creation failed for '{config.name}': {api_error}"
154
+ self.logger.error(error_msg)
155
+ raise BlackAntDockerError(error_msg) from api_error
156
+ except Exception as error:
157
+ error_msg = f"Unexpected error creating service '{config.name}': {error}"
158
+ self.logger.error(error_msg)
159
+ raise BlackAntDockerError(error_msg) from error
160
+
161
+ def get_service(self, service_id: str) -> Optional[DockerServiceObject]:
162
+ """Szolgáltatás lekérése ID vagy név alapján.
163
+
164
+ Args:
165
+ service_id: Service ID vagy név
166
+
167
+ Returns:
168
+ DockerServiceObject vagy None ha nem található
169
+
170
+ Raises:
171
+ BlackAntDockerError: Docker API hiba esetén (kivéve NotFound)
172
+ """
173
+ try:
174
+ service = self.__docker_client.services.get(service_id)
175
+ self.logger.debug(f"Service found: {service_id}")
176
+ return service
177
+
178
+ except NotFound:
179
+ self.logger.warning(f"Service not found: {service_id}")
180
+ return None
181
+
182
+ except APIError as api_error:
183
+ error_msg = f"Error getting service '{service_id}': {api_error}"
184
+ self.logger.error(error_msg)
185
+ raise BlackAntDockerError(error_msg) from api_error
186
+
187
+ def list_services(self, filters: Optional[Dict[str, Any]] = None) -> List[DockerServiceObject]:
188
+ """Szolgáltatások listázása.
189
+
190
+ Args:
191
+ filters: Docker API szűrők (pl. {'label': 'env=prod'})
192
+
193
+ Returns:
194
+ List[DockerServiceObject]: Service objektumok listája
195
+
196
+ Raises:
197
+ BlackAntDockerError: Docker API hiba esetén
198
+ """
199
+ try:
200
+ services = self.__docker_client.services.list(filters=filters)
201
+ self.logger.debug(f"Found {len(services)} services")
202
+ return services
203
+
204
+ except APIError as api_error:
205
+ error_msg = f"Error listing services: {api_error}"
206
+ self.logger.error(error_msg)
207
+ raise BlackAntDockerError(error_msg) from api_error
208
+
209
+ def remove_service(self, service_id: str) -> bool:
210
+ """Szolgáltatás eltávolítása.
211
+
212
+ Args:
213
+ service_id: Service ID vagy név
214
+
215
+ Returns:
216
+ bool: True ha sikeresen eltávolítva, False ha nem található
217
+
218
+ Raises:
219
+ BlackAntDockerError: Docker API hiba esetén (kivéve NotFound)
220
+ """
221
+ try:
222
+ service = self.get_service(service_id)
223
+ if service:
224
+ service.remove()
225
+ self.logger.info(f"Service removed successfully: {service_id}")
226
+ return True
227
+ else:
228
+ self.logger.warning(f"Service not found for removal: {service_id}")
229
+ return False
230
+
231
+ except APIError as api_error:
232
+ error_msg = f"Error removing service '{service_id}': {api_error}"
233
+ self.logger.error(error_msg)
234
+ raise BlackAntDockerError(error_msg) from api_error
235
+
236
+ def update_service(self, service_id: str, **kwargs) -> Optional[DockerServiceObject]:
237
+ """Szolgáltatás frissítése.
238
+
239
+ Args:
240
+ service_id: Service ID vagy név
241
+ **kwargs: Frissítendő paraméterek (image, env, labels, stb.)
242
+
243
+ Returns:
244
+ DockerServiceObject vagy None ha nem található
245
+
246
+ Raises:
247
+ BlackAntDockerError: Docker API hiba esetén
248
+ """
249
+ try:
250
+ service = self.get_service(service_id)
251
+ if service:
252
+ # Refresh service object to get latest version
253
+ service.reload()
254
+
255
+ # Update service with provided parameters
256
+ service.update(**kwargs)
257
+
258
+ self.logger.info(f"Service updated successfully: {service_id}")
259
+ return service
260
+ else:
261
+ self.logger.warning(f"Service not found for update: {service_id}")
262
+ return None
263
+
264
+ except APIError as api_error:
265
+ error_msg = f"Error updating service '{service_id}': {api_error}"
266
+ self.logger.error(error_msg)
267
+ raise BlackAntDockerError(error_msg) from api_error
268
+
269
+ def scale_service(self, service_id: str, replicas: int) -> Optional[DockerServiceObject]:
270
+ """Szolgáltatás méretezése (replicas számának változtatása).
271
+
272
+ Args:
273
+ service_id: Service ID vagy név
274
+ replicas: Új replicas szám
275
+
276
+ Returns:
277
+ DockerServiceObject vagy None ha nem található
278
+
279
+ Raises:
280
+ BlackAntDockerError: Docker API hiba esetén
281
+ """
282
+ try:
283
+ service = self.get_service(service_id)
284
+ if service:
285
+ # Scale service by updating replica count
286
+ mode = docker.types.ServiceMode('replicated', replicas)
287
+ service.update(mode=mode)
288
+
289
+ self.logger.info(f"Service scaled successfully: {service_id} -> {replicas} replicas")
290
+ return service
291
+ else:
292
+ self.logger.warning(f"Service not found for scaling: {service_id}")
293
+ return None
294
+
295
+ except APIError as api_error:
296
+ error_msg = f"Error scaling service '{service_id}': {api_error}"
297
+ self.logger.error(error_msg)
298
+ raise BlackAntDockerError(error_msg) from api_error
299
+
300
+ def get_service_logs(self, service_id: str, **kwargs) -> str:
301
+ """Szolgáltatás naplóinak lekérése.
302
+
303
+ Args:
304
+ service_id: Service ID vagy név
305
+ **kwargs: Log opciók (tail, since, follow, stb.)
306
+
307
+ Returns:
308
+ str: Service logs
309
+
310
+ Raises:
311
+ BlackAntDockerError: Ha a service nem található vagy API hiba
312
+ """
313
+ try:
314
+ service = self.get_service(service_id)
315
+ if service:
316
+ logs = service.logs(**kwargs)
317
+ # Convert bytes to string if needed
318
+ if isinstance(logs, bytes):
319
+ logs = logs.decode('utf-8')
320
+
321
+ self.logger.debug(f"Retrieved logs for service: {service_id}")
322
+ return logs
323
+ else:
324
+ raise BlackAntDockerError(f"Service not found: {service_id}")
325
+
326
+ except APIError as api_error:
327
+ error_msg = f"Error getting logs for service '{service_id}': {api_error}"
328
+ self.logger.error(error_msg)
329
+ raise BlackAntDockerError(error_msg) from api_error
330
+
331
+ def is_service_running(self, service_id: str) -> bool:
332
+ """Ellenőrzi, hogy a szolgáltatásnak van-e futó task-ja.
333
+
334
+ Args:
335
+ service_id: Service ID vagy név
336
+
337
+ Returns:
338
+ bool: True ha van futó task, False egyébként
339
+ """
340
+ try:
341
+ service = self.get_service(service_id)
342
+ if service:
343
+ # Check if service has any running tasks
344
+ tasks = service.tasks()
345
+ for task in tasks:
346
+ if task.get('Status', {}).get('State') == 'running':
347
+ return True
348
+ return False
349
+ else:
350
+ return False
351
+
352
+ except Exception as error:
353
+ self.logger.error(f"Error checking service status '{service_id}': {error}")
354
+ return False
355
+
356
+ def wait_for_service_ready(self, service_id: str, timeout: int = 300) -> bool:
357
+ """Várakozás amíg a szolgáltatás futó állapotba kerül.
358
+
359
+ Args:
360
+ service_id: Service ID vagy név
361
+ timeout: Maximum várakozási idő másodpercekben
362
+
363
+ Returns:
364
+ bool: True ha a szolgáltatás futó állapotba került, False timeout esetén
365
+ """
366
+ self.logger.info(f"Waiting for service to become ready: {service_id}")
367
+
368
+ start_time = time.time()
369
+ while time.time() - start_time < timeout:
370
+ if self.is_service_running(service_id):
371
+ self.logger.info(f"Service is ready: {service_id}")
372
+ return True
373
+
374
+ # Random sleep to avoid overwhelming the API
375
+ sleep_time = random.uniform(0.5, 2.0)
376
+ time.sleep(sleep_time)
377
+
378
+ self.logger.warning(f"Service did not become ready within {timeout}s: {service_id}")
379
+ return False
380
+
381
+ def get_service_tasks(self, service_id: str) -> List[Dict[str, Any]]:
382
+ """Szolgáltatás task-jainak lekérése.
383
+
384
+ Args:
385
+ service_id: Service ID vagy név
386
+
387
+ Returns:
388
+ List[Dict]: Service task információk listája
389
+
390
+ Raises:
391
+ BlackAntDockerError: Ha a service nem található vagy API hiba
392
+ """
393
+ try:
394
+ service = self.get_service(service_id)
395
+ if service:
396
+ tasks = service.tasks()
397
+ self.logger.debug(f"Found {len(tasks)} tasks for service: {service_id}")
398
+ return tasks
399
+ else:
400
+ raise BlackAntDockerError(f"Service not found: {service_id}")
401
+
402
+ except APIError as api_error:
403
+ error_msg = f"Error getting tasks for service '{service_id}': {api_error}"
404
+ self.logger.error(error_msg)
405
+ raise BlackAntDockerError(error_msg) from api_error
406
+
407
+ def __enter__(self):
408
+ """Context manager entry."""
409
+ return self
410
+
411
+ def __exit__(self, exc_type, exc_val, exc_tb):
412
+ """Context manager exit - cleanup resources."""
413
+ if hasattr(self.__docker_client, "close"):
414
+ self.__docker_client.close()