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/__init__.py +178 -4
- pactown/cli.py +539 -37
- pactown/config.py +12 -11
- pactown/deploy/__init__.py +17 -3
- pactown/deploy/base.py +35 -33
- pactown/deploy/compose.py +59 -58
- pactown/deploy/docker.py +40 -41
- pactown/deploy/kubernetes.py +43 -42
- pactown/deploy/podman.py +55 -56
- pactown/deploy/quadlet.py +1021 -0
- pactown/deploy/quadlet_api.py +533 -0
- pactown/deploy/quadlet_shell.py +557 -0
- pactown/events.py +1066 -0
- pactown/fast_start.py +514 -0
- pactown/generator.py +31 -30
- pactown/llm.py +450 -0
- pactown/markpact_blocks.py +50 -0
- pactown/network.py +59 -38
- pactown/orchestrator.py +90 -93
- pactown/parallel.py +40 -40
- pactown/platform.py +146 -0
- pactown/registry/__init__.py +1 -1
- pactown/registry/client.py +45 -46
- pactown/registry/models.py +25 -25
- pactown/registry/server.py +24 -24
- pactown/resolver.py +30 -30
- pactown/runner_api.py +458 -0
- pactown/sandbox_manager.py +480 -79
- pactown/security.py +682 -0
- pactown/service_runner.py +1201 -0
- pactown/user_isolation.py +458 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/METADATA +65 -9
- pactown-0.1.47.dist-info/RECORD +36 -0
- pactown-0.1.47.dist-info/entry_points.txt +5 -0
- pactown-0.1.4.dist-info/RECORD +0 -24
- pactown-0.1.4.dist-info/entry_points.txt +0 -3
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/WHEEL +0 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/licenses/LICENSE +0 -0
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
|
|
8
|
-
from dataclasses import dataclass
|
|
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
|
|