pactown 0.1.4__py3-none-any.whl → 0.1.47__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.
pactown/network.py CHANGED
@@ -2,13 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import socket
6
5
  import json
7
- import os
8
- from dataclasses import dataclass, field
6
+ import socket
7
+ from dataclasses import dataclass
9
8
  from pathlib import Path
10
- from typing import Optional
11
9
  from threading import Lock
10
+ from typing import Optional
12
11
 
13
12
 
14
13
  @dataclass
@@ -18,11 +17,11 @@ class ServiceEndpoint:
18
17
  host: str
19
18
  port: int
20
19
  health_check: Optional[str] = None
21
-
20
+
22
21
  @property
23
22
  def url(self) -> str:
24
23
  return f"http://{self.host}:{self.port}"
25
-
24
+
26
25
  @property
27
26
  def health_url(self) -> Optional[str]:
28
27
  if self.health_check:
@@ -30,15 +29,29 @@ class ServiceEndpoint:
30
29
  return None
31
30
 
32
31
 
32
+ # Minimum safe port - below this are privileged/system ports
33
+ MIN_SAFE_PORT = 1024
34
+
35
+
33
36
  class PortAllocator:
34
- """Allocates free ports dynamically."""
37
+ """Allocates free ports dynamically.
35
38
 
39
+ By default, only allocates ports >= 1024 to avoid conflicts with
40
+ system services (SSH, HTTP, HTTPS, databases, etc.).
41
+ """
42
+
36
43
  def __init__(self, start_port: int = 10000, end_port: int = 65000):
44
+ # Safety: ensure we don't allocate privileged ports
45
+ if start_port < MIN_SAFE_PORT:
46
+ start_port = MIN_SAFE_PORT
47
+ if end_port > 65535:
48
+ end_port = 65535
49
+
37
50
  self.start_port = start_port
38
51
  self.end_port = end_port
39
52
  self._allocated: set[int] = set()
40
53
  self._lock = Lock()
41
-
54
+
42
55
  def is_port_free(self, port: int) -> bool:
43
56
  """Check if a port is available."""
44
57
  if port in self._allocated:
@@ -50,33 +63,35 @@ class PortAllocator:
50
63
  return True
51
64
  except OSError:
52
65
  return False
53
-
66
+
54
67
  def allocate(self, preferred_port: Optional[int] = None) -> int:
55
68
  """
56
69
  Allocate a free port.
57
-
70
+
58
71
  If preferred_port is given and available, use it.
59
72
  Otherwise, find the next available port.
73
+
74
+ Note: Ports below MIN_SAFE_PORT (1024) are rejected for safety.
60
75
  """
61
76
  with self._lock:
62
- # Try preferred port first
63
- if preferred_port and self.is_port_free(preferred_port):
77
+ # Try preferred port first (but only if it's in safe range)
78
+ if preferred_port and preferred_port >= MIN_SAFE_PORT and self.is_port_free(preferred_port):
64
79
  self._allocated.add(preferred_port)
65
80
  return preferred_port
66
-
81
+
67
82
  # Find next available port
68
83
  for port in range(self.start_port, self.end_port):
69
84
  if self.is_port_free(port):
70
85
  self._allocated.add(port)
71
86
  return port
72
-
87
+
73
88
  raise RuntimeError("No free ports available")
74
-
89
+
75
90
  def release(self, port: int) -> None:
76
91
  """Release an allocated port."""
77
92
  with self._lock:
78
93
  self._allocated.discard(port)
79
-
94
+
80
95
  def release_all(self) -> None:
81
96
  """Release all allocated ports."""
82
97
  with self._lock:
@@ -86,13 +101,13 @@ class PortAllocator:
86
101
  class ServiceRegistry:
87
102
  """
88
103
  Local service registry for name-based service discovery.
89
-
104
+
90
105
  Services register with their name and get assigned a port.
91
106
  Other services can look up endpoints by name.
92
107
  """
93
-
108
+
94
109
  def __init__(
95
- self,
110
+ self,
96
111
  storage_path: Optional[Path] = None,
97
112
  host: str = "127.0.0.1",
98
113
  ):
@@ -102,7 +117,7 @@ class ServiceRegistry:
102
117
  self._port_allocator = PortAllocator()
103
118
  self._lock = Lock()
104
119
  self._load()
105
-
120
+
106
121
  def _load(self) -> None:
107
122
  """Load service registry from disk."""
108
123
  if self.storage_path.exists():
@@ -118,7 +133,7 @@ class ServiceRegistry:
118
133
  )
119
134
  except (json.JSONDecodeError, KeyError):
120
135
  pass
121
-
136
+
122
137
  def _save(self) -> None:
123
138
  """Persist service registry to disk."""
124
139
  data = {
@@ -134,7 +149,7 @@ class ServiceRegistry:
134
149
  }
135
150
  with open(self.storage_path, "w") as f:
136
151
  json.dump(data, f, indent=2)
137
-
152
+
138
153
  def register(
139
154
  self,
140
155
  name: str,
@@ -143,7 +158,7 @@ class ServiceRegistry:
143
158
  ) -> ServiceEndpoint:
144
159
  """
145
160
  Register a service and allocate a port.
146
-
161
+
147
162
  If preferred_port is available, use it. Otherwise, allocate dynamically.
148
163
  """
149
164
  with self._lock:
@@ -155,22 +170,22 @@ class ServiceRegistry:
155
170
  return existing
156
171
  # Port is taken, need to reallocate
157
172
  self._port_allocator.release(existing.port)
158
-
173
+
159
174
  # Allocate port
160
175
  port = self._port_allocator.allocate(preferred_port)
161
-
176
+
162
177
  endpoint = ServiceEndpoint(
163
178
  name=name,
164
179
  host=self.host,
165
180
  port=port,
166
181
  health_check=health_check,
167
182
  )
168
-
183
+
169
184
  self._services[name] = endpoint
170
185
  self._save()
171
-
186
+
172
187
  return endpoint
173
-
188
+
174
189
  def unregister(self, name: str) -> None:
175
190
  """Unregister a service."""
176
191
  with self._lock:
@@ -178,35 +193,35 @@ class ServiceRegistry:
178
193
  self._port_allocator.release(self._services[name].port)
179
194
  del self._services[name]
180
195
  self._save()
181
-
196
+
182
197
  def get(self, name: str) -> Optional[ServiceEndpoint]:
183
198
  """Get service endpoint by name."""
184
199
  return self._services.get(name)
185
-
200
+
186
201
  def get_url(self, name: str) -> Optional[str]:
187
202
  """Get service URL by name."""
188
203
  svc = self.get(name)
189
204
  return svc.url if svc else None
190
-
205
+
191
206
  def list_services(self) -> list[ServiceEndpoint]:
192
207
  """List all registered services."""
193
208
  return list(self._services.values())
194
-
209
+
195
210
  def get_environment(self, service_name: str, dependencies: list[str]) -> dict[str, str]:
196
211
  """
197
212
  Get environment variables for a service.
198
-
213
+
199
214
  Injects URLs for all dependencies as environment variables.
200
215
  """
201
216
  env = {}
202
-
217
+
203
218
  # Add own endpoint info
204
219
  if service_name in self._services:
205
220
  svc = self._services[service_name]
206
221
  env["MARKPACT_PORT"] = str(svc.port)
207
222
  env["SERVICE_NAME"] = service_name
208
223
  env["SERVICE_URL"] = svc.url
209
-
224
+
210
225
  # Add dependency URLs
211
226
  for dep_name in dependencies:
212
227
  if dep_name in self._services:
@@ -216,9 +231,9 @@ class ServiceRegistry:
216
231
  env[f"{env_key}_URL"] = dep.url
217
232
  env[f"{env_key}_HOST"] = dep.host
218
233
  env[f"{env_key}_PORT"] = str(dep.port)
219
-
234
+
220
235
  return env
221
-
236
+
222
237
  def clear(self) -> None:
223
238
  """Clear all registrations."""
224
239
  with self._lock:
@@ -229,7 +244,13 @@ class ServiceRegistry:
229
244
 
230
245
 
231
246
  def find_free_port(start: int = 10000, end: int = 65000) -> int:
232
- """Find a single free port."""
247
+ """Find a single free port.
248
+
249
+ Note: start will be clamped to MIN_SAFE_PORT (1024) minimum for safety.
250
+ """
251
+ # Safety: ensure start is at least MIN_SAFE_PORT
252
+ if start < MIN_SAFE_PORT:
253
+ start = MIN_SAFE_PORT
233
254
  allocator = PortAllocator(start, end)
234
255
  return allocator.allocate()
235
256