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,457 @@
1
+ """Docker configuration management for BlackAnt SDK."""
2
+
3
+ import os
4
+ from typing import Optional, Dict, Any
5
+ from dataclasses import dataclass, field
6
+
7
+ from blackant.utils.logging import get_logger
8
+
9
+ try:
10
+ import docker
11
+ import docker.tls
12
+ except ImportError:
13
+ docker = None
14
+
15
+
16
+ @dataclass
17
+ class DockerDaemonConfig:
18
+ """Remote Docker daemon configuration.
19
+
20
+ Supports both nginx proxy (recommended) and direct TLS connection
21
+ to remote Docker daemon as per BlackAnt architecture specification.
22
+ """
23
+
24
+ # Remote Docker daemon host
25
+ remote_host: str = field(default_factory=lambda: os.getenv(
26
+ "DOCKER_DAEMON_HOST",
27
+ "localhost"
28
+ ))
29
+
30
+ remote_port: int = field(default_factory=lambda: int(os.getenv(
31
+ "DOCKER_DAEMON_PORT",
32
+ "80"
33
+ )))
34
+
35
+ # Use nginx proxy (recommended for JWT token authentication)
36
+ use_nginx_proxy: bool = field(default_factory=lambda: os.getenv(
37
+ "DOCKER_USE_NGINX_PROXY",
38
+ "true"
39
+ ).lower() == "true")
40
+
41
+ # Nginx proxy URL (when use_nginx_proxy=True)
42
+ nginx_proxy_url: str = field(default_factory=lambda: os.getenv(
43
+ "DOCKER_NGINX_PROXY_URL",
44
+ "http://localhost:80/api/docker"
45
+ ))
46
+
47
+ # TLS configuration (when use_nginx_proxy=False)
48
+ use_tls: bool = field(default_factory=lambda: os.getenv(
49
+ "DOCKER_USE_TLS",
50
+ "false"
51
+ ).lower() == "true")
52
+
53
+ ca_cert: Optional[str] = field(default_factory=lambda: os.getenv("DOCKER_CA_CERT"))
54
+ client_cert: Optional[str] = field(default_factory=lambda: os.getenv("DOCKER_CLIENT_CERT"))
55
+ client_key: Optional[str] = field(default_factory=lambda: os.getenv("DOCKER_CLIENT_KEY"))
56
+
57
+ # Connection timeout
58
+ timeout: int = field(default_factory=lambda: int(os.getenv(
59
+ "DOCKER_DAEMON_TIMEOUT",
60
+ "600"
61
+ )))
62
+
63
+ @property
64
+ def base_url(self) -> str:
65
+ """Get Docker daemon base URL.
66
+
67
+ Returns:
68
+ str: Docker daemon URL (nginx proxy or direct connection)
69
+ """
70
+ if self.use_nginx_proxy:
71
+ return self.nginx_proxy_url
72
+
73
+ protocol = "https" if self.use_tls else "http"
74
+ return f"{protocol}://{self.remote_host}:{self.remote_port}"
75
+
76
+ def get_tls_config(self) -> Optional['docker.tls.TLSConfig']:
77
+ """Get TLS configuration for Docker client.
78
+
79
+ Returns:
80
+ TLSConfig or None if TLS not used or nginx proxy is enabled
81
+ """
82
+ if docker is None:
83
+ return None
84
+
85
+ if not self.use_tls or self.use_nginx_proxy:
86
+ return None
87
+
88
+ if not all([self.ca_cert, self.client_cert, self.client_key]):
89
+ raise ValueError("TLS enabled but certificate paths not configured")
90
+
91
+ return docker.tls.TLSConfig(
92
+ client_cert=(self.client_cert, self.client_key),
93
+ ca_cert=self.ca_cert,
94
+ verify=True
95
+ )
96
+
97
+
98
+ @dataclass
99
+ class DockerRegistryConfig:
100
+ """Docker registry configuration data class."""
101
+
102
+ url: str = "env.blackant.app"
103
+ namespace: str = "science_module"
104
+ username: Optional[str] = None
105
+ password: Optional[str] = None
106
+
107
+ def __post_init__(self):
108
+ """Post-initialization to load from environment if not provided."""
109
+ if not self.username:
110
+ self.username = os.getenv("DOCKER_REGISTRY_USER")
111
+ if not self.password:
112
+ self.password = os.getenv("DOCKER_REGISTRY_PASS")
113
+
114
+
115
+ @dataclass
116
+ class SdkServicesConfig:
117
+ """SDK Services configuration for HTTP-based operations.
118
+
119
+ When enabled, registry push and role assignment operations are
120
+ delegated to sdk-services backend, keeping credentials server-side.
121
+ """
122
+
123
+ # SDK Services URL
124
+ url: str = field(default_factory=lambda: os.getenv(
125
+ "SDK_SERVICES_URL",
126
+ "" # Empty = disabled, use direct Docker/Keycloak access
127
+ ))
128
+
129
+ # Use SDK Services for registry push (instead of direct Docker push)
130
+ use_for_registry: bool = field(default_factory=lambda: os.getenv(
131
+ "SDK_SERVICES_REGISTRY",
132
+ "false"
133
+ ).lower() == "true")
134
+
135
+ # Use SDK Services for role assignment (instead of direct Keycloak access)
136
+ use_for_roles: bool = field(default_factory=lambda: os.getenv(
137
+ "SDK_SERVICES_ROLES",
138
+ "false"
139
+ ).lower() == "true")
140
+
141
+ # Request timeout in seconds
142
+ timeout: int = field(default_factory=lambda: int(os.getenv(
143
+ "SDK_SERVICES_TIMEOUT",
144
+ "600"
145
+ )))
146
+
147
+ @property
148
+ def enabled(self) -> bool:
149
+ """Check if SDK Services is configured and enabled."""
150
+ return bool(self.url) and (self.use_for_registry or self.use_for_roles)
151
+
152
+ @property
153
+ def registry_push_url(self) -> str:
154
+ """Get the registry push endpoint URL."""
155
+ return f"{self.url}/api/sdk_services/registry/push" if self.url else ""
156
+
157
+ @property
158
+ def roles_assign_url(self) -> str:
159
+ """Get the role assignment endpoint URL."""
160
+ return f"{self.url}/api/sdk_services/roles/assign" if self.url else ""
161
+
162
+
163
+ @dataclass
164
+ class DockerBuildConfig:
165
+ """Docker build configuration data class."""
166
+
167
+ timeout: int = 600 # seconds
168
+ build_args: Dict[str, str] = None
169
+ labels: Dict[str, str] = None
170
+ pull: bool = True # Pull base image updates
171
+ rm: bool = True # Remove intermediate containers
172
+ forcerm: bool = True # Always remove intermediate containers
173
+ push_retries: int = 3
174
+ push_timeout: int = 300 # seconds
175
+
176
+ def __post_init__(self):
177
+ """Initialize default values for mutable fields."""
178
+ if self.build_args is None:
179
+ self.build_args = {
180
+ "BLACKANT_VERSION": "1.0.0",
181
+ "BUILD_PLATFORM": "linux/amd64"
182
+ }
183
+
184
+ if self.labels is None:
185
+ self.labels = {
186
+ "blackant.sdk.version": "1.0.0",
187
+ "blackant.build.automated": "true"
188
+ }
189
+
190
+
191
+ class DockerConfig:
192
+ """Central Docker configuration manager for BlackAnt SDK."""
193
+
194
+ def __init__(self):
195
+ """Initialize Docker configuration."""
196
+ self.logger = get_logger("docker-config")
197
+ self._daemon_config = None
198
+ self._registry_config = None
199
+ self._build_config = None
200
+ self._sdk_services_config = None
201
+
202
+ # Load configuration on init
203
+ self._load_config()
204
+
205
+ def _load_config(self) -> None:
206
+ """Load configuration from environment and defaults."""
207
+ self.logger.debug("Loading Docker configuration")
208
+
209
+ # Remote Docker daemon configuration
210
+ self._daemon_config = DockerDaemonConfig()
211
+
212
+ # Registry configuration
213
+ self._registry_config = DockerRegistryConfig(
214
+ url=os.getenv("DOCKER_REGISTRY_URL", "env.blackant.app"),
215
+ namespace=os.getenv("DOCKER_REGISTRY_NAMESPACE", "science_module"),
216
+ username=os.getenv("DOCKER_REGISTRY_USER"),
217
+ password=os.getenv("DOCKER_REGISTRY_PASS")
218
+ )
219
+
220
+ # Build configuration
221
+ self._build_config = DockerBuildConfig(
222
+ timeout=int(os.getenv("DOCKER_BUILD_TIMEOUT", "600")),
223
+ push_retries=int(os.getenv("DOCKER_PUSH_RETRIES", "3")),
224
+ push_timeout=int(os.getenv("DOCKER_PUSH_TIMEOUT", "300"))
225
+ )
226
+
227
+ # SDK Services configuration
228
+ self._sdk_services_config = SdkServicesConfig()
229
+
230
+ self.logger.info(f"Docker config loaded: daemon={self._daemon_config.base_url}, registry={self._registry_config.url}")
231
+
232
+ if self._sdk_services_config.enabled:
233
+ self.logger.info(f"SDK Services enabled: {self._sdk_services_config.url}")
234
+
235
+ @property
236
+ def daemon(self) -> DockerDaemonConfig:
237
+ """Get Docker daemon configuration."""
238
+ return self._daemon_config
239
+
240
+ @property
241
+ def registry(self) -> DockerRegistryConfig:
242
+ """Get registry configuration."""
243
+ return self._registry_config
244
+
245
+ @property
246
+ def build(self) -> DockerBuildConfig:
247
+ """Get build configuration."""
248
+ return self._build_config
249
+
250
+ @property
251
+ def sdk_services(self) -> SdkServicesConfig:
252
+ """Get SDK Services configuration."""
253
+ return self._sdk_services_config
254
+
255
+ def get_full_image_name(self, service_name: str, tag: str = "latest") -> str:
256
+ """Generate full Docker image name with registry and namespace.
257
+
258
+ Args:
259
+ service_name: Service name
260
+ tag: Image tag
261
+
262
+ Returns:
263
+ str: Full image name (e.g., "env.blackant.app/systemdevelopers/service:tag")
264
+ """
265
+ return f"{self.registry.url}/{self.registry.namespace}/{service_name}:{tag}"
266
+
267
+ def get_registry_credentials(self) -> Dict[str, Optional[str]]:
268
+ """Get registry credentials for Docker login.
269
+
270
+ Returns:
271
+ dict: Registry credentials or empty dict if not configured
272
+ """
273
+ if self.registry.username and self.registry.password:
274
+ return {
275
+ "username": self.registry.username,
276
+ "password": self.registry.password,
277
+ "registry": self.registry.url
278
+ }
279
+ return {}
280
+
281
+ def get_build_args(self, service_name: str, additional_args: Optional[Dict[str, str]] = None) -> Dict[str, str]:
282
+ """Get build arguments for Docker build.
283
+
284
+ Args:
285
+ service_name: Service name to add to build args
286
+ additional_args: Additional build arguments to merge
287
+
288
+ Returns:
289
+ dict: Combined build arguments
290
+ """
291
+ args = {
292
+ **self.build.build_args,
293
+ "SERVICE_NAME": service_name
294
+ }
295
+
296
+ if additional_args:
297
+ args.update(additional_args)
298
+
299
+ return args
300
+
301
+ def get_build_labels(self, service_name: str, additional_labels: Optional[Dict[str, str]] = None) -> Dict[str, str]:
302
+ """Get build labels for Docker build.
303
+
304
+ Args:
305
+ service_name: Service name to add to labels
306
+ additional_labels: Additional labels to merge
307
+
308
+ Returns:
309
+ dict: Combined build labels
310
+ """
311
+ from datetime import datetime
312
+
313
+ labels = {
314
+ **self.build.labels,
315
+ "blackant.service.name": service_name,
316
+ "blackant.build.date": datetime.now().isoformat(),
317
+ "blackant.registry": self.registry.url
318
+ }
319
+
320
+ if additional_labels:
321
+ labels.update(additional_labels)
322
+
323
+ return labels
324
+
325
+ def validate_config(self) -> bool:
326
+ """Validate Docker configuration.
327
+
328
+ Returns:
329
+ bool: True if configuration is valid
330
+
331
+ Raises:
332
+ ValueError: If configuration is invalid
333
+ """
334
+ # Registry validation
335
+ if not self.registry.url:
336
+ raise ValueError("Docker registry URL cannot be empty")
337
+
338
+ if not self.registry.namespace:
339
+ raise ValueError("Docker registry namespace cannot be empty")
340
+
341
+ # Build validation
342
+ if self.build.timeout <= 0:
343
+ raise ValueError("Docker build timeout must be positive")
344
+
345
+ if self.build.push_retries < 0:
346
+ raise ValueError("Docker push retries cannot be negative")
347
+
348
+ if self.build.push_timeout <= 0:
349
+ raise ValueError("Docker push timeout must be positive")
350
+
351
+ self.logger.debug("Docker configuration validation passed")
352
+ return True
353
+
354
+ def to_dict(self) -> Dict[str, Any]:
355
+ """Convert configuration to dictionary.
356
+
357
+ Returns:
358
+ dict: Configuration as dictionary (without sensitive data)
359
+ """
360
+ return {
361
+ "daemon": {
362
+ "base_url": self.daemon.base_url,
363
+ "use_nginx_proxy": self.daemon.use_nginx_proxy,
364
+ "use_tls": self.daemon.use_tls,
365
+ "timeout": self.daemon.timeout
366
+ },
367
+ "registry": {
368
+ "url": self.registry.url,
369
+ "namespace": self.registry.namespace,
370
+ "has_credentials": bool(self.registry.username and self.registry.password)
371
+ },
372
+ "build": {
373
+ "timeout": self.build.timeout,
374
+ "push_retries": self.build.push_retries,
375
+ "push_timeout": self.build.push_timeout,
376
+ "pull": self.build.pull,
377
+ "rm": self.build.rm,
378
+ "forcerm": self.build.forcerm
379
+ }
380
+ }
381
+
382
+ def __str__(self) -> str:
383
+ """String representation of configuration."""
384
+ return f"DockerConfig(daemon={self.daemon.base_url}, registry={self.registry.url}/{self.registry.namespace})"
385
+
386
+
387
+ # Global configuration instance - singleton pattern használata
388
+ _docker_config_instance = None
389
+
390
+
391
+ def get_docker_config() -> DockerConfig:
392
+ """Get global Docker configuration instance.
393
+
394
+ Returns:
395
+ DockerConfig: Global configuration instance
396
+ """
397
+ global _docker_config_instance
398
+
399
+ if _docker_config_instance is None:
400
+ _docker_config_instance = DockerConfig()
401
+ _docker_config_instance.validate_config()
402
+
403
+ return _docker_config_instance
404
+
405
+
406
+ def reload_docker_config() -> DockerConfig:
407
+ """Reload Docker configuration from environment.
408
+
409
+ Returns:
410
+ DockerConfig: Newly loaded configuration instance
411
+ """
412
+ global _docker_config_instance
413
+ _docker_config_instance = None
414
+ return get_docker_config()
415
+
416
+
417
+ # Environment variable documentation
418
+ """
419
+ Environment Variables for Docker Configuration:
420
+
421
+ === Remote Docker Daemon Configuration ===
422
+ DOCKER_USE_NGINX_PROXY: Use nginx proxy for Docker daemon access (default: true)
423
+ DOCKER_NGINX_PROXY_URL: Nginx proxy URL (default: http://localhost)
424
+ DOCKER_DAEMON_HOST: Remote Docker daemon host (default: localhost)
425
+ DOCKER_DAEMON_PORT: Remote Docker daemon port (default: 80)
426
+ DOCKER_USE_TLS: Use TLS for direct Docker connection (default: false)
427
+ DOCKER_CA_CERT: Path to CA certificate for TLS
428
+ DOCKER_CLIENT_CERT: Path to client certificate for TLS
429
+ DOCKER_CLIENT_KEY: Path to client key for TLS
430
+ DOCKER_DAEMON_TIMEOUT: Docker daemon timeout in seconds (default: 600)
431
+
432
+ === Docker Registry Configuration ===
433
+ DOCKER_REGISTRY_URL: Docker registry URL (default: env.blackant.app)
434
+ DOCKER_REGISTRY_NAMESPACE: Registry namespace (default: systemdevelopers)
435
+ DOCKER_REGISTRY_USER: Registry username for authentication
436
+ DOCKER_REGISTRY_PASS: Registry password for authentication
437
+
438
+ === Build Configuration ===
439
+ DOCKER_BUILD_TIMEOUT: Build timeout in seconds (default: 600)
440
+ DOCKER_PUSH_RETRIES: Number of push retries (default: 3)
441
+ DOCKER_PUSH_TIMEOUT: Push timeout in seconds (default: 300)
442
+
443
+ Example - Nginx Proxy (Recommended):
444
+ export DOCKER_USE_NGINX_PROXY=true
445
+ export DOCKER_NGINX_PROXY_URL="http://localhost/api/docker"
446
+ export DOCKER_REGISTRY_URL="env.blackant.app"
447
+ export DOCKER_REGISTRY_NAMESPACE="systemdevelopers"
448
+
449
+ Example - Direct TLS Connection:
450
+ export DOCKER_USE_NGINX_PROXY=false
451
+ export DOCKER_DAEMON_HOST="docker.blackant.app"
452
+ export DOCKER_DAEMON_PORT="2376"
453
+ export DOCKER_USE_TLS=true
454
+ export DOCKER_CA_CERT="/etc/blackant/certs/ca.pem"
455
+ export DOCKER_CLIENT_CERT="/etc/blackant/certs/client-cert.pem"
456
+ export DOCKER_CLIENT_KEY="/etc/blackant/certs/client-key.pem"
457
+ """
@@ -0,0 +1,107 @@
1
+ """Keycloak Admin Configuration
2
+
3
+ Configuration for Keycloak service account used for role management.
4
+
5
+ Author: Balázs Milán (milan.balazs@uni-obuda.hu)
6
+ """
7
+
8
+ import os
9
+ from typing import Optional
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass
14
+ class KeycloakAdminConfig:
15
+ """Configuration for Keycloak Admin API.
16
+
17
+ Service account credentials for role management operations.
18
+
19
+ Attributes:
20
+ server_url: Keycloak server URL (e.g., "https://keycloak.company.com/auth")
21
+ realm_name: Realm name (e.g., "blackant")
22
+ client_id: Service account client ID
23
+ client_secret: Service account client secret
24
+ verify_ssl: Whether to verify SSL certificates
25
+ timeout: Request timeout in seconds
26
+ """
27
+
28
+ server_url: str
29
+ realm_name: str
30
+ client_id: str
31
+ client_secret: str
32
+ verify_ssl: bool = True
33
+ timeout: int = 30
34
+
35
+ @classmethod
36
+ def from_env(cls) -> 'KeycloakAdminConfig':
37
+ """Create configuration from environment variables.
38
+
39
+ Required environment variables:
40
+ - KEYCLOAK_SERVER_URL: Keycloak server URL
41
+ - KEYCLOAK_REALM: Realm name
42
+ - KEYCLOAK_ADMIN_CLIENT_ID: Service account client ID
43
+ - KEYCLOAK_ADMIN_CLIENT_SECRET: Service account client secret
44
+
45
+ Optional:
46
+ - KEYCLOAK_VERIFY_SSL: "true" or "false" (default: true)
47
+ - KEYCLOAK_TIMEOUT: Timeout in seconds (default: 30)
48
+
49
+ Returns:
50
+ KeycloakAdminConfig instance
51
+
52
+ Raises:
53
+ ValueError: If required environment variables are missing
54
+ """
55
+ server_url = os.getenv('KEYCLOAK_SERVER_URL')
56
+ realm_name = os.getenv('KEYCLOAK_REALM')
57
+ client_id = os.getenv('KEYCLOAK_ADMIN_CLIENT_ID')
58
+ client_secret = os.getenv('KEYCLOAK_ADMIN_CLIENT_SECRET')
59
+
60
+ # Validate required vars
61
+ missing = []
62
+ if not server_url:
63
+ missing.append('KEYCLOAK_SERVER_URL')
64
+ if not realm_name:
65
+ missing.append('KEYCLOAK_REALM')
66
+ if not client_id:
67
+ missing.append('KEYCLOAK_ADMIN_CLIENT_ID')
68
+ if not client_secret:
69
+ missing.append('KEYCLOAK_ADMIN_CLIENT_SECRET')
70
+
71
+ if missing:
72
+ raise ValueError(
73
+ f"Missing required environment variables: {', '.join(missing)}"
74
+ )
75
+
76
+ # Optional vars
77
+ verify_ssl = os.getenv('KEYCLOAK_VERIFY_SSL', 'true').lower() == 'true'
78
+ timeout = int(os.getenv('KEYCLOAK_TIMEOUT', '30'))
79
+
80
+ return cls(
81
+ server_url=server_url,
82
+ realm_name=realm_name,
83
+ client_id=client_id,
84
+ client_secret=client_secret,
85
+ verify_ssl=verify_ssl,
86
+ timeout=timeout
87
+ )
88
+
89
+ def validate(self) -> None:
90
+ """Validate configuration values.
91
+
92
+ Raises:
93
+ ValueError: If configuration is invalid
94
+ """
95
+ if not self.server_url.startswith(('http://', 'https://')):
96
+ raise ValueError(
97
+ f"Invalid server URL: {self.server_url} "
98
+ "(must start with http:// or https://)"
99
+ )
100
+
101
+ if len(self.client_secret) < 16:
102
+ raise ValueError(
103
+ "Client secret must be at least 16 characters long"
104
+ )
105
+
106
+ if self.timeout < 1 or self.timeout > 300:
107
+ raise ValueError("Timeout must be between 1 and 300 seconds")
@@ -0,0 +1,12 @@
1
+ # src/blackant/docker/__init__.py
2
+ """Docker integration module for BlackAnt SDK."""
3
+
4
+ from .client import BlackAntDockerClient
5
+ from .dao import (
6
+ DockerDAO, DockerConnectionError, ImageConfig, ResourceConfig, ServiceConfig
7
+ )
8
+
9
+ __all__ = [
10
+ "BlackAntDockerClient", "DockerDAO", "DockerConnectionError",
11
+ "ImageConfig", "ResourceConfig", "ServiceConfig"
12
+ ]