ergon-framework-python 0.1.0__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 (82) hide show
  1. ergon/__init__.py +13 -0
  2. ergon/bootstrap/src/__project__/__init__.py +0 -0
  3. ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +124 -0
  4. ergon/bootstrap/src/__project__/_observability/grafana.yaml +17 -0
  5. ergon/bootstrap/src/__project__/_observability/loki.yaml +48 -0
  6. ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +53 -0
  7. ergon/bootstrap/src/__project__/_observability/prometheus.yaml +11 -0
  8. ergon/bootstrap/src/__project__/_observability/tempo.yaml +24 -0
  9. ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
  10. ergon/bootstrap/src/__project__/main.py +9 -0
  11. ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
  12. ergon/bootstrap/src/__project__/tasks/constants.py +13 -0
  13. ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
  14. ergon/bootstrap/src/__project__/tasks/example_task/config.py +4 -0
  15. ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +4 -0
  16. ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +4 -0
  17. ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +5 -0
  18. ergon/bootstrap/src/__project__/tasks/example_task/task.py +1 -0
  19. ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
  20. ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
  21. ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
  22. ergon/bootstrap/src/__project__/tasks/settings.py +5 -0
  23. ergon/cli.py +174 -0
  24. ergon/connector/__init__.py +64 -0
  25. ergon/connector/connector.py +97 -0
  26. ergon/connector/excel/__init__.py +18 -0
  27. ergon/connector/excel/connector.py +175 -0
  28. ergon/connector/excel/models.py +24 -0
  29. ergon/connector/excel/service.py +98 -0
  30. ergon/connector/pipefy/__init__.py +21 -0
  31. ergon/connector/pipefy/async_connector.py +48 -0
  32. ergon/connector/pipefy/async_service.py +907 -0
  33. ergon/connector/pipefy/connector.py +36 -0
  34. ergon/connector/pipefy/models.py +48 -0
  35. ergon/connector/pipefy/service.py +1016 -0
  36. ergon/connector/pipefy/version.py +1 -0
  37. ergon/connector/postgres/__init__.py +11 -0
  38. ergon/connector/postgres/async_connector.py +119 -0
  39. ergon/connector/postgres/async_service.py +116 -0
  40. ergon/connector/postgres/models.py +34 -0
  41. ergon/connector/rabbitmq/__init__.py +25 -0
  42. ergon/connector/rabbitmq/async_connector.py +120 -0
  43. ergon/connector/rabbitmq/async_service.py +417 -0
  44. ergon/connector/rabbitmq/connector.py +54 -0
  45. ergon/connector/rabbitmq/helper.py +14 -0
  46. ergon/connector/rabbitmq/models.py +92 -0
  47. ergon/connector/rabbitmq/service.py +199 -0
  48. ergon/connector/sqs/__init__.py +15 -0
  49. ergon/connector/sqs/async_connector.py +120 -0
  50. ergon/connector/sqs/async_service.py +246 -0
  51. ergon/connector/sqs/connector.py +120 -0
  52. ergon/connector/sqs/models.py +36 -0
  53. ergon/connector/sqs/service.py +219 -0
  54. ergon/connector/transaction.py +14 -0
  55. ergon/py.typed +0 -0
  56. ergon/service/__init__.py +5 -0
  57. ergon/service/service.py +17 -0
  58. ergon/task/__init__.py +13 -0
  59. ergon/task/base.py +222 -0
  60. ergon/task/exceptions.py +217 -0
  61. ergon/task/helpers.py +691 -0
  62. ergon/task/manager.py +85 -0
  63. ergon/task/mixins/__init__.py +13 -0
  64. ergon/task/mixins/consumer.py +858 -0
  65. ergon/task/mixins/metrics.py +457 -0
  66. ergon/task/mixins/producer.py +486 -0
  67. ergon/task/policies.py +229 -0
  68. ergon/task/runner.py +386 -0
  69. ergon/task/utils.py +64 -0
  70. ergon/telemetry/__init__.py +7 -0
  71. ergon/telemetry/_resource.py +13 -0
  72. ergon/telemetry/logging.py +370 -0
  73. ergon/telemetry/metrics.py +101 -0
  74. ergon/telemetry/tracing.py +152 -0
  75. ergon/utils/__init__.py +5 -0
  76. ergon/utils/env.py +26 -0
  77. ergon_framework_python-0.1.0.dist-info/METADATA +449 -0
  78. ergon_framework_python-0.1.0.dist-info/RECORD +82 -0
  79. ergon_framework_python-0.1.0.dist-info/WHEEL +5 -0
  80. ergon_framework_python-0.1.0.dist-info/entry_points.txt +2 -0
  81. ergon_framework_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  82. ergon_framework_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,36 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class SQSClient(BaseModel):
7
+ region_name: str = Field(description="AWS region name (e.g. us-east-1)")
8
+ aws_access_key_id: Optional[str] = Field(default=None, description="AWS access key ID. None = use env/IAM role")
9
+ aws_secret_access_key: Optional[str] = Field(
10
+ default=None, description="AWS secret access key. None = use env/IAM role"
11
+ )
12
+ aws_session_token: Optional[str] = Field(default=None, description="AWS session token for temporary credentials")
13
+ endpoint_url: Optional[str] = Field(default=None, description="Custom endpoint URL (e.g. LocalStack)")
14
+ queue_url: Optional[str] = Field(default=None, description="Default SQS queue URL, can be overridden per-call")
15
+
16
+
17
+ class SQSConsumerConfig(BaseModel):
18
+ queue_url: Optional[str] = Field(default=None, description="Override default queue URL for consuming")
19
+ wait_time_seconds: int = Field(default=20, description="Long polling wait time in seconds (0-20)")
20
+ visibility_timeout: Optional[int] = Field(
21
+ default=None, description="Override queue's default visibility timeout in seconds"
22
+ )
23
+ attribute_names: List[str] = Field(default=["All"], description="System attribute names to include in response")
24
+ message_attribute_names: List[str] = Field(
25
+ default=["All"], description="Message attribute names to include in response"
26
+ )
27
+
28
+
29
+ class SQSProducerConfig(BaseModel):
30
+ queue_url: Optional[str] = Field(default=None, description="Override default queue URL for producing")
31
+ delay_seconds: int = Field(default=0, description="Delay in seconds before message becomes visible (0-900)")
32
+ message_group_id: Optional[str] = Field(default=None, description="Message group ID for FIFO queues")
33
+ message_deduplication_id: Optional[str] = Field(default=None, description="Deduplication ID for FIFO queues")
34
+ message_attributes: Optional[Dict[str, Dict]] = Field(
35
+ default=None, description="Custom message attributes to attach to sent messages"
36
+ )
@@ -0,0 +1,219 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import boto3
6
+
7
+ from .models import SQSClient
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SQSService:
13
+ def __init__(self, client: SQSClient) -> None:
14
+ self.client = client
15
+
16
+ kwargs: Dict[str, Any] = {"region_name": client.region_name}
17
+ if client.aws_access_key_id:
18
+ kwargs["aws_access_key_id"] = client.aws_access_key_id
19
+ if client.aws_secret_access_key:
20
+ kwargs["aws_secret_access_key"] = client.aws_secret_access_key
21
+ if client.aws_session_token:
22
+ kwargs["aws_session_token"] = client.aws_session_token
23
+ if client.endpoint_url:
24
+ kwargs["endpoint_url"] = client.endpoint_url
25
+
26
+ self._sqs = boto3.client("sqs", **kwargs)
27
+
28
+ # ---------- Receive ----------
29
+
30
+ def receive_messages(
31
+ self,
32
+ queue_url: Optional[str] = None,
33
+ max_number_of_messages: int = 1,
34
+ wait_time_seconds: int = 20,
35
+ visibility_timeout: Optional[int] = None,
36
+ attribute_names: Optional[List[str]] = None,
37
+ message_attribute_names: Optional[List[str]] = None,
38
+ ) -> List[Dict[str, Any]]:
39
+ """
40
+ Receive up to `max_number_of_messages` from the queue.
41
+
42
+ SQS caps MaxNumberOfMessages at 10 per API call, so for larger
43
+ batch sizes this loops until the requested count is reached or
44
+ the queue returns no more messages.
45
+ """
46
+ url = queue_url or self.client.queue_url
47
+ if not url:
48
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
49
+
50
+ collected: List[Dict[str, Any]] = []
51
+ remaining = max_number_of_messages
52
+
53
+ first_call = True
54
+ while remaining > 0:
55
+ fetch_count = min(remaining, 10)
56
+
57
+ params: Dict[str, Any] = {
58
+ "QueueUrl": url,
59
+ "MaxNumberOfMessages": fetch_count,
60
+ "WaitTimeSeconds": wait_time_seconds if first_call else 0,
61
+ "AttributeNames": attribute_names or ["All"],
62
+ "MessageAttributeNames": message_attribute_names or ["All"],
63
+ }
64
+ if visibility_timeout is not None:
65
+ params["VisibilityTimeout"] = visibility_timeout
66
+
67
+ response = self._sqs.receive_message(**params)
68
+ messages = response.get("Messages", [])
69
+ first_call = False
70
+
71
+ if not messages:
72
+ break
73
+
74
+ collected.extend(messages)
75
+ remaining -= len(messages)
76
+
77
+ if len(messages) < fetch_count:
78
+ break
79
+
80
+ return collected
81
+
82
+ # ---------- Send ----------
83
+
84
+ def send_message(
85
+ self,
86
+ message_body: Any,
87
+ queue_url: Optional[str] = None,
88
+ delay_seconds: int = 0,
89
+ message_attributes: Optional[Dict[str, Dict]] = None,
90
+ message_group_id: Optional[str] = None,
91
+ message_deduplication_id: Optional[str] = None,
92
+ ) -> Dict[str, Any]:
93
+ url = queue_url or self.client.queue_url
94
+ if not url:
95
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
96
+
97
+ body = json.dumps(message_body) if not isinstance(message_body, str) else message_body
98
+
99
+ params: Dict[str, Any] = {
100
+ "QueueUrl": url,
101
+ "MessageBody": body,
102
+ "DelaySeconds": delay_seconds,
103
+ }
104
+ if message_attributes:
105
+ params["MessageAttributes"] = message_attributes
106
+ if message_group_id:
107
+ params["MessageGroupId"] = message_group_id
108
+ if message_deduplication_id:
109
+ params["MessageDeduplicationId"] = message_deduplication_id
110
+
111
+ return self._sqs.send_message(**params)
112
+
113
+ def send_message_batch(
114
+ self,
115
+ entries: List[Dict[str, Any]],
116
+ queue_url: Optional[str] = None,
117
+ ) -> Dict[str, Any]:
118
+ """
119
+ Send messages in batches of up to 10 (SQS limit).
120
+ Each entry must have at minimum 'Id' and 'MessageBody'.
121
+ """
122
+ url = queue_url or self.client.queue_url
123
+ if not url:
124
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
125
+
126
+ successful = []
127
+ failed = []
128
+
129
+ for i in range(0, len(entries), 10):
130
+ chunk = entries[i : i + 10]
131
+ response = self._sqs.send_message_batch(QueueUrl=url, Entries=chunk)
132
+ successful.extend(response.get("Successful", []))
133
+ failed.extend(response.get("Failed", []))
134
+
135
+ return {"Successful": successful, "Failed": failed}
136
+
137
+ # ---------- Delete ----------
138
+
139
+ def delete_message(self, receipt_handle: str, queue_url: Optional[str] = None) -> None:
140
+ url = queue_url or self.client.queue_url
141
+ if not url:
142
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
143
+
144
+ self._sqs.delete_message(QueueUrl=url, ReceiptHandle=receipt_handle)
145
+
146
+ def delete_message_batch(
147
+ self,
148
+ entries: List[Dict[str, str]],
149
+ queue_url: Optional[str] = None,
150
+ ) -> Dict[str, Any]:
151
+ """
152
+ Delete messages in batches of up to 10.
153
+ Each entry must have 'Id' and 'ReceiptHandle'.
154
+ """
155
+ url = queue_url or self.client.queue_url
156
+ if not url:
157
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
158
+
159
+ successful = []
160
+ failed = []
161
+
162
+ for i in range(0, len(entries), 10):
163
+ chunk = entries[i : i + 10]
164
+ response = self._sqs.delete_message_batch(QueueUrl=url, Entries=chunk)
165
+ successful.extend(response.get("Successful", []))
166
+ failed.extend(response.get("Failed", []))
167
+
168
+ return {"Successful": successful, "Failed": failed}
169
+
170
+ # ---------- Queue Management ----------
171
+
172
+ def list_queues(self, prefix: Optional[str] = None) -> List[str]:
173
+ params: Dict[str, Any] = {}
174
+ if prefix:
175
+ params["QueueNamePrefix"] = prefix
176
+
177
+ response = self._sqs.list_queues(**params)
178
+ return response.get("QueueUrls", [])
179
+
180
+ def get_queue_attributes(
181
+ self,
182
+ attribute_names: Optional[List[str]] = None,
183
+ queue_url: Optional[str] = None,
184
+ ) -> Dict[str, str]:
185
+ url = queue_url or self.client.queue_url
186
+ if not url:
187
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
188
+
189
+ response = self._sqs.get_queue_attributes(
190
+ QueueUrl=url,
191
+ AttributeNames=attribute_names or ["All"],
192
+ )
193
+ return response.get("Attributes", {})
194
+
195
+ def create_queue(
196
+ self,
197
+ queue_name: str,
198
+ attributes: Optional[Dict[str, str]] = None,
199
+ ) -> str:
200
+ params: Dict[str, Any] = {"QueueName": queue_name}
201
+ if attributes:
202
+ params["Attributes"] = attributes
203
+
204
+ response = self._sqs.create_queue(**params)
205
+ return response["QueueUrl"]
206
+
207
+ def delete_queue(self, queue_url: Optional[str] = None) -> None:
208
+ url = queue_url or self.client.queue_url
209
+ if not url:
210
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
211
+
212
+ self._sqs.delete_queue(QueueUrl=url)
213
+
214
+ def purge_queue(self, queue_url: Optional[str] = None) -> None:
215
+ url = queue_url or self.client.queue_url
216
+ if not url:
217
+ raise ValueError("queue_url must be provided either in SQSClient or per-call")
218
+
219
+ self._sqs.purge_queue(QueueUrl=url)
@@ -0,0 +1,14 @@
1
+ from typing import Any, Dict
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class Transaction(BaseModel):
7
+ """
8
+ Generic transaction model.
9
+ """
10
+
11
+ id: str
12
+ payload: Any
13
+ metadata: Dict[str, Any] = Field(default_factory=dict)
14
+ model_config = ConfigDict(frozen=True)
ergon/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ from .service import ServiceConfig
2
+
3
+ __all__ = [
4
+ "ServiceConfig",
5
+ ]
@@ -0,0 +1,17 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ServiceConfig(BaseModel):
7
+ """
8
+ Generic service configuration.
9
+
10
+ service: class implementing a service
11
+ args: positional arguments passed to service's __init__
12
+ kwargs: keyword arguments passed to service's __init__
13
+ """
14
+
15
+ service: type
16
+ args: tuple[Any, ...] = ()
17
+ kwargs: dict[str, Any] = {}
ergon/task/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from . import exceptions, helpers, mixins, policies, utils
2
+ from .base import TaskConfig
3
+ from .manager import manager
4
+
5
+ __all__ = [
6
+ "manager",
7
+ "TaskConfig",
8
+ "mixins",
9
+ "policies",
10
+ "exceptions",
11
+ "helpers",
12
+ "utils",
13
+ ]
ergon/task/base.py ADDED
@@ -0,0 +1,222 @@
1
+ import socket
2
+ from abc import ABC, ABCMeta, abstractmethod
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+ from .. import telemetry
8
+ from ..connector import Connector, ConnectorConfig
9
+ from ..service import ServiceConfig
10
+
11
+
12
+ class TaskConfig(BaseModel):
13
+ """
14
+ Declarative configuration for a task.
15
+ Specifies:
16
+ - which Task class to run
17
+ - input/output connectors
18
+ - logger configuration
19
+ - worker scaling
20
+ """
21
+
22
+ name: str
23
+ task: object
24
+ max_workers: int = 1
25
+ connectors: Dict[str, ConnectorConfig] = Field(default_factory=dict)
26
+ services: Dict[str, ServiceConfig] = Field(default_factory=dict)
27
+ policies: List[Any] = Field(default_factory=list)
28
+ logging: Optional[telemetry.logging.LoggingConfig] = None
29
+ metrics: Optional[telemetry.metrics.MetricsConfig] = None
30
+ tracing: Optional[telemetry.tracing.TracingConfig] = None
31
+
32
+ @model_validator(mode="after")
33
+ def validate(self) -> "TaskConfig":
34
+ if not self.connectors and not self.services:
35
+ raise ValueError("Task must define at least one connector or service.")
36
+
37
+ if self.max_workers < 1:
38
+ raise ValueError("max_workers must be at least 1.")
39
+
40
+ return self
41
+
42
+
43
+ class TaskMeta(ABCMeta):
44
+ """
45
+ Metaclass that injects connectors, services and execution policies into task instances.
46
+ Consumed by BaseTask and BaseAsyncTask.
47
+ """
48
+
49
+ def __call__(
50
+ cls,
51
+ connectors: Dict[str, Connector],
52
+ services: Dict[str, Any],
53
+ policies: List[Any],
54
+ worker_id: Optional[int] = None,
55
+ task_config: Optional[TaskConfig] = None,
56
+ *args,
57
+ **kwargs,
58
+ ):
59
+ # ---------------------------------------------------------
60
+ # Create instance normally
61
+ # ---------------------------------------------------------
62
+ self = super().__call__(
63
+ connectors=connectors,
64
+ services=services,
65
+ policies=policies,
66
+ worker_id=worker_id,
67
+ task_config=task_config,
68
+ *args,
69
+ **kwargs,
70
+ )
71
+
72
+ # ---------------------------------------------------------
73
+ # Store DI containers
74
+ # ---------------------------------------------------------
75
+ self.connectors = connectors
76
+ self.services = services
77
+ self.policies = policies
78
+ self.worker_id = worker_id
79
+ self.task_config = task_config
80
+
81
+ # ---------------------------------------------------------
82
+ # Expose connectors as attributes
83
+ # Example: pipefy_connector, rabbitmq_connector
84
+ # ---------------------------------------------------------
85
+ for name, conn in connectors.items():
86
+ setattr(self, f"{name}_connector", conn)
87
+
88
+ # ---------------------------------------------------------
89
+ # Expose services as attributes
90
+ # Example: openai_service, s3_service
91
+ # ---------------------------------------------------------
92
+ for name, service in services.items():
93
+ setattr(self, f"{name}_service", service)
94
+
95
+ # ---------------------------------------------------------
96
+ # Expose policies as attributes
97
+ # Example: policy_name
98
+ # ---------------------------------------------------------
99
+ for policy in policies:
100
+ name = policy.name or policy.__class__.__name__
101
+ setattr(self, f"{name}_policy", policy)
102
+
103
+ # ---------------------------------------------------------
104
+ # Task identity
105
+ # ---------------------------------------------------------
106
+ self.name = getattr(self, "name", cls.__name__)
107
+
108
+ return self
109
+
110
+
111
+ # ---------- BASE TASKS ----------
112
+
113
+
114
+ class BaseTask(ABC, metaclass=TaskMeta):
115
+ """
116
+ Base class for all tasks in the Ergon Task Framework.
117
+
118
+ Behavior (produce/consume) is added via mixins:
119
+ - ProduceMixin
120
+ - ConsumeMixin
121
+ """
122
+
123
+ name: str = "base"
124
+ connectors: Dict[str, Connector]
125
+ services: Dict[str, Any]
126
+ policies: List[Any]
127
+ worker_id: Optional[int]
128
+ task_config: Optional["TaskConfig"]
129
+
130
+ def __init__(
131
+ self,
132
+ connectors: Dict[str, Connector],
133
+ services: Dict[str, Any],
134
+ policies: List[Any],
135
+ worker_id: Optional[int] = None,
136
+ task_config: Optional["TaskConfig"] = None,
137
+ *args,
138
+ **kwargs,
139
+ ):
140
+ # Don't pass args/kwargs to object.__init__()
141
+ super().__init__()
142
+
143
+ @abstractmethod
144
+ def execute(self) -> Any:
145
+ """Main entry point for running the task."""
146
+ raise NotImplementedError
147
+
148
+ def exit(self):
149
+ """
150
+ Gracefully exit the task.
151
+
152
+ Tasks may override this to:
153
+ - close resources
154
+ - flush buffers
155
+ - notify services
156
+
157
+ IMPORTANT: By default, this does NOT kill the interpreter.
158
+ """
159
+ return
160
+
161
+
162
+ class BaseAsyncTask(ABC, metaclass=TaskMeta):
163
+ """
164
+ Asynchronous base class for all async tasks in the Ergon Task Framework.
165
+
166
+ Behavior (async produce/consume) will be added via async mixins.
167
+ """
168
+
169
+ name: str = "base_async"
170
+ connectors: Dict[str, Any]
171
+ services: Dict[str, Any]
172
+ policies: List[Any]
173
+ worker_id: Optional[int]
174
+ task_config: Optional["TaskConfig"]
175
+
176
+ def __init__(
177
+ self,
178
+ connectors: Dict[str, Any],
179
+ services: Dict[str, Any],
180
+ policies: List[Any],
181
+ worker_id: Optional[int] = None,
182
+ task_config: Optional["TaskConfig"] = None,
183
+ *args,
184
+ **kwargs,
185
+ ):
186
+ # Don't pass args/kwargs to object.__init__()
187
+ super().__init__()
188
+
189
+ @abstractmethod
190
+ async def execute(self) -> Any:
191
+ """Main async entry point for running the task."""
192
+ raise NotImplementedError
193
+
194
+ async def exit(self):
195
+ """Optional async cleanup hook."""
196
+ return
197
+
198
+
199
+ class TaskExecMetadata(BaseModel):
200
+ task_name: str
201
+ execution_id: str
202
+ execution_start_time: str
203
+ pid: int
204
+ worker_id: Optional[int]
205
+ host_name: str = Field(default_factory=lambda: TaskExecMetadata._get_host_name())
206
+ host_ip: str = Field(default_factory=lambda: TaskExecMetadata._get_host_ip())
207
+
208
+ # ---------- STATIC HELPERS ----------
209
+ @staticmethod
210
+ def _get_host_ip() -> str:
211
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
212
+ try:
213
+ s.connect(("8.8.8.8", 80))
214
+ return s.getsockname()[0]
215
+ except Exception:
216
+ return "127.0.0.1"
217
+ finally:
218
+ s.close()
219
+
220
+ @staticmethod
221
+ def _get_host_name() -> str:
222
+ return socket.gethostname()