rpp-protocol 0.1.5__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.
- rpp/__init__.py +60 -0
- rpp/adapters/__init__.py +14 -0
- rpp/adapters/filesystem.py +151 -0
- rpp/adapters/memory.py +95 -0
- rpp/address.py +249 -0
- rpp/cli.py +608 -0
- rpp/i18n.py +426 -0
- rpp/resolver.py +216 -0
- rpp/visual.py +389 -0
- rpp_protocol-0.1.5.dist-info/METADATA +390 -0
- rpp_protocol-0.1.5.dist-info/RECORD +14 -0
- rpp_protocol-0.1.5.dist-info/WHEEL +4 -0
- rpp_protocol-0.1.5.dist-info/entry_points.txt +2 -0
- rpp_protocol-0.1.5.dist-info/licenses/LICENSE +219 -0
rpp/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RPP - Rotational Packet Protocol
|
|
3
|
+
|
|
4
|
+
A 28-bit semantic addressing system for consent-aware routing.
|
|
5
|
+
|
|
6
|
+
RPP IS:
|
|
7
|
+
- A deterministic 28-bit semantic address
|
|
8
|
+
- A resolver that returns allow / deny / route
|
|
9
|
+
- A bridge to existing storage backends
|
|
10
|
+
|
|
11
|
+
RPP IS NOT:
|
|
12
|
+
- A storage system
|
|
13
|
+
- A database
|
|
14
|
+
- An identity provider
|
|
15
|
+
- A policy DSL
|
|
16
|
+
- An AI system
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.5"
|
|
20
|
+
|
|
21
|
+
from rpp.address import (
|
|
22
|
+
RPPAddress,
|
|
23
|
+
encode,
|
|
24
|
+
decode,
|
|
25
|
+
from_components,
|
|
26
|
+
from_raw,
|
|
27
|
+
is_valid_address,
|
|
28
|
+
MAX_ADDRESS,
|
|
29
|
+
MAX_SHELL,
|
|
30
|
+
MAX_THETA,
|
|
31
|
+
MAX_PHI,
|
|
32
|
+
MAX_HARMONIC,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from rpp.resolver import (
|
|
36
|
+
RPPResolver,
|
|
37
|
+
ResolveResult,
|
|
38
|
+
resolve,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Version
|
|
43
|
+
"__version__",
|
|
44
|
+
# Address
|
|
45
|
+
"RPPAddress",
|
|
46
|
+
"encode",
|
|
47
|
+
"decode",
|
|
48
|
+
"from_components",
|
|
49
|
+
"from_raw",
|
|
50
|
+
"is_valid_address",
|
|
51
|
+
"MAX_ADDRESS",
|
|
52
|
+
"MAX_SHELL",
|
|
53
|
+
"MAX_THETA",
|
|
54
|
+
"MAX_PHI",
|
|
55
|
+
"MAX_HARMONIC",
|
|
56
|
+
# Resolver
|
|
57
|
+
"RPPResolver",
|
|
58
|
+
"ResolveResult",
|
|
59
|
+
"resolve",
|
|
60
|
+
]
|
rpp/adapters/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RPP Backend Adapters
|
|
3
|
+
|
|
4
|
+
Adapters provide the bridge between RPP addresses and actual storage backends.
|
|
5
|
+
The resolver selects adapters based on shell value.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from rpp.adapters.memory import MemoryAdapter
|
|
9
|
+
from rpp.adapters.filesystem import FilesystemAdapter
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"MemoryAdapter",
|
|
13
|
+
"FilesystemAdapter",
|
|
14
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem Backend Adapter
|
|
3
|
+
|
|
4
|
+
A local filesystem storage backend.
|
|
5
|
+
Uses pathlib for cross-platform compatibility (Windows, Linux, macOS).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FilesystemAdapter:
|
|
13
|
+
"""
|
|
14
|
+
Filesystem storage adapter.
|
|
15
|
+
|
|
16
|
+
Suitable for:
|
|
17
|
+
- Shell 1 (Warm tier)
|
|
18
|
+
- Local development
|
|
19
|
+
- Persistent storage
|
|
20
|
+
|
|
21
|
+
Uses pathlib for Windows/Linux/macOS compatibility.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name: str = "filesystem"
|
|
25
|
+
|
|
26
|
+
def __init__(self, base_path: Optional[str] = None) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Initialize filesystem adapter.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
base_path: Base directory for storage. If None, uses temp directory.
|
|
32
|
+
"""
|
|
33
|
+
if base_path is None:
|
|
34
|
+
# Use a cross-platform temp location
|
|
35
|
+
import tempfile
|
|
36
|
+
self._base = Path(tempfile.gettempdir()) / "rpp_storage"
|
|
37
|
+
else:
|
|
38
|
+
self._base = Path(base_path)
|
|
39
|
+
|
|
40
|
+
# Ensure base directory exists
|
|
41
|
+
self._base.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def base_path(self) -> Path:
|
|
45
|
+
"""Return the base storage path."""
|
|
46
|
+
return self._base
|
|
47
|
+
|
|
48
|
+
def _resolve_path(self, path: str) -> Path:
|
|
49
|
+
"""Resolve a path relative to base."""
|
|
50
|
+
# Normalize path separators
|
|
51
|
+
normalized = path.replace("\\", "/")
|
|
52
|
+
# Remove leading slashes
|
|
53
|
+
normalized = normalized.lstrip("/")
|
|
54
|
+
return self._base / normalized
|
|
55
|
+
|
|
56
|
+
def read(self, path: str) -> Optional[bytes]:
|
|
57
|
+
"""
|
|
58
|
+
Read data from filesystem.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path: Storage path (relative to base)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Data bytes if found, None otherwise
|
|
65
|
+
"""
|
|
66
|
+
full_path = self._resolve_path(path)
|
|
67
|
+
if not full_path.exists():
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
return full_path.read_bytes()
|
|
71
|
+
except (OSError, IOError):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def write(self, path: str, data: bytes) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Write data to filesystem.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
path: Storage path (relative to base)
|
|
80
|
+
data: Data to store
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True on success, False on failure
|
|
84
|
+
"""
|
|
85
|
+
full_path = self._resolve_path(path)
|
|
86
|
+
try:
|
|
87
|
+
# Ensure parent directories exist
|
|
88
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
full_path.write_bytes(data)
|
|
90
|
+
return True
|
|
91
|
+
except (OSError, IOError):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def delete(self, path: str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Delete data from filesystem.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
path: Storage path (relative to base)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if deleted, False if not found or error
|
|
103
|
+
"""
|
|
104
|
+
full_path = self._resolve_path(path)
|
|
105
|
+
if not full_path.exists():
|
|
106
|
+
return False
|
|
107
|
+
try:
|
|
108
|
+
full_path.unlink()
|
|
109
|
+
return True
|
|
110
|
+
except (OSError, IOError):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def exists(self, path: str) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Check if path exists.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path: Storage path (relative to base)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if exists
|
|
122
|
+
"""
|
|
123
|
+
return self._resolve_path(path).exists()
|
|
124
|
+
|
|
125
|
+
def is_available(self) -> bool:
|
|
126
|
+
"""Check if adapter is available (base path exists and is writable)."""
|
|
127
|
+
try:
|
|
128
|
+
return self._base.exists() and self._base.is_dir()
|
|
129
|
+
except (OSError, IOError):
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def list_paths(self, pattern: str = "**/*") -> list:
|
|
133
|
+
"""
|
|
134
|
+
List stored paths matching pattern.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
pattern: Glob pattern (default: all files)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of relative path strings
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
paths = []
|
|
144
|
+
for p in self._base.glob(pattern):
|
|
145
|
+
if p.is_file():
|
|
146
|
+
# Return path relative to base
|
|
147
|
+
rel_path = p.relative_to(self._base)
|
|
148
|
+
paths.append(str(rel_path))
|
|
149
|
+
return paths
|
|
150
|
+
except (OSError, IOError):
|
|
151
|
+
return []
|
rpp/adapters/memory.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-Memory Backend Adapter
|
|
3
|
+
|
|
4
|
+
A simple in-memory storage backend for testing and hot-tier data.
|
|
5
|
+
No external dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryAdapter:
|
|
12
|
+
"""
|
|
13
|
+
In-memory storage adapter.
|
|
14
|
+
|
|
15
|
+
Suitable for:
|
|
16
|
+
- Shell 0 (Hot tier)
|
|
17
|
+
- Testing
|
|
18
|
+
- Ephemeral data
|
|
19
|
+
|
|
20
|
+
Thread-safety: NOT thread-safe. Use external locking if needed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: str = "memory"
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._storage: Dict[str, bytes] = {}
|
|
27
|
+
|
|
28
|
+
def read(self, path: str) -> Optional[bytes]:
|
|
29
|
+
"""
|
|
30
|
+
Read data from memory.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
path: Storage path
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Data bytes if found, None otherwise
|
|
37
|
+
"""
|
|
38
|
+
return self._storage.get(path)
|
|
39
|
+
|
|
40
|
+
def write(self, path: str, data: bytes) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Write data to memory.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
path: Storage path
|
|
46
|
+
data: Data to store
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True on success
|
|
50
|
+
"""
|
|
51
|
+
self._storage[path] = data
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def delete(self, path: str) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Delete data from memory.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
path: Storage path
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True if deleted, False if not found
|
|
63
|
+
"""
|
|
64
|
+
if path in self._storage:
|
|
65
|
+
del self._storage[path]
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def exists(self, path: str) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Check if path exists.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
path: Storage path
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if exists
|
|
78
|
+
"""
|
|
79
|
+
return path in self._storage
|
|
80
|
+
|
|
81
|
+
def is_available(self) -> bool:
|
|
82
|
+
"""Check if adapter is available. Always True for memory."""
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def clear(self) -> None:
|
|
86
|
+
"""Clear all stored data."""
|
|
87
|
+
self._storage.clear()
|
|
88
|
+
|
|
89
|
+
def list_paths(self) -> list:
|
|
90
|
+
"""List all stored paths."""
|
|
91
|
+
return list(self._storage.keys())
|
|
92
|
+
|
|
93
|
+
def size(self) -> int:
|
|
94
|
+
"""Return number of stored items."""
|
|
95
|
+
return len(self._storage)
|
rpp/address.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RPP Address Encoding/Decoding
|
|
3
|
+
|
|
4
|
+
Implements the 28-bit RPP address format:
|
|
5
|
+
[31:28] Reserved (must be 0)
|
|
6
|
+
[27:26] Shell (2 bits, 0-3)
|
|
7
|
+
[25:17] Theta (9 bits, 0-511)
|
|
8
|
+
[16:8] Phi (9 bits, 0-511)
|
|
9
|
+
[7:0] Harmonic (8 bits, 0-255)
|
|
10
|
+
|
|
11
|
+
This module is pure Python with no external dependencies.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
|
|
17
|
+
# Constants
|
|
18
|
+
MAX_ADDRESS = 0x0FFFFFFF # 28 bits max
|
|
19
|
+
MAX_SHELL = 3
|
|
20
|
+
MAX_THETA = 511
|
|
21
|
+
MAX_PHI = 511
|
|
22
|
+
MAX_HARMONIC = 255
|
|
23
|
+
|
|
24
|
+
# Bit positions
|
|
25
|
+
SHELL_SHIFT = 26
|
|
26
|
+
THETA_SHIFT = 17
|
|
27
|
+
PHI_SHIFT = 8
|
|
28
|
+
HARMONIC_SHIFT = 0
|
|
29
|
+
|
|
30
|
+
# Masks
|
|
31
|
+
SHELL_MASK = 0x3
|
|
32
|
+
THETA_MASK = 0x1FF
|
|
33
|
+
PHI_MASK = 0x1FF
|
|
34
|
+
HARMONIC_MASK = 0xFF
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class RPPAddress:
|
|
39
|
+
"""
|
|
40
|
+
Immutable RPP address with decoded components.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
shell: Radial depth / storage tier (0-3)
|
|
44
|
+
theta: Angular sector (0-511)
|
|
45
|
+
phi: Grounding level (0-511)
|
|
46
|
+
harmonic: Frequency / mode (0-255)
|
|
47
|
+
raw: Original 28-bit integer
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
shell: int
|
|
51
|
+
theta: int
|
|
52
|
+
phi: int
|
|
53
|
+
harmonic: int
|
|
54
|
+
raw: int
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
"""Validate components are in range."""
|
|
58
|
+
if not (0 <= self.shell <= MAX_SHELL):
|
|
59
|
+
raise ValueError(f"Shell must be 0-{MAX_SHELL}, got {self.shell}")
|
|
60
|
+
if not (0 <= self.theta <= MAX_THETA):
|
|
61
|
+
raise ValueError(f"Theta must be 0-{MAX_THETA}, got {self.theta}")
|
|
62
|
+
if not (0 <= self.phi <= MAX_PHI):
|
|
63
|
+
raise ValueError(f"Phi must be 0-{MAX_PHI}, got {self.phi}")
|
|
64
|
+
if not (0 <= self.harmonic <= MAX_HARMONIC):
|
|
65
|
+
raise ValueError(f"Harmonic must be 0-{MAX_HARMONIC}, got {self.harmonic}")
|
|
66
|
+
if not (0 <= self.raw <= MAX_ADDRESS):
|
|
67
|
+
raise ValueError(f"Raw address must be 0-{hex(MAX_ADDRESS)}, got {hex(self.raw)}")
|
|
68
|
+
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
return f"RPP(shell={self.shell}, theta={self.theta}, phi={self.phi}, harmonic={self.harmonic}) = {self.to_hex()}"
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"RPPAddress(shell={self.shell}, theta={self.theta}, phi={self.phi}, harmonic={self.harmonic}, raw={hex(self.raw)})"
|
|
74
|
+
|
|
75
|
+
def to_hex(self) -> str:
|
|
76
|
+
"""Return address as zero-padded hex string."""
|
|
77
|
+
return f"0x{self.raw:07X}"
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict:
|
|
80
|
+
"""Return address as dictionary (JSON-serializable)."""
|
|
81
|
+
return {
|
|
82
|
+
"shell": self.shell,
|
|
83
|
+
"theta": self.theta,
|
|
84
|
+
"phi": self.phi,
|
|
85
|
+
"harmonic": self.harmonic,
|
|
86
|
+
"address": self.to_hex(),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def sector_name(self) -> str:
|
|
91
|
+
"""Return canonical sector name for theta."""
|
|
92
|
+
if self.theta < 64:
|
|
93
|
+
return "Gene"
|
|
94
|
+
elif self.theta < 128:
|
|
95
|
+
return "Memory"
|
|
96
|
+
elif self.theta < 192:
|
|
97
|
+
return "Witness"
|
|
98
|
+
elif self.theta < 256:
|
|
99
|
+
return "Dream"
|
|
100
|
+
elif self.theta < 320:
|
|
101
|
+
return "Bridge"
|
|
102
|
+
elif self.theta < 384:
|
|
103
|
+
return "Guardian"
|
|
104
|
+
elif self.theta < 448:
|
|
105
|
+
return "Emergence"
|
|
106
|
+
else:
|
|
107
|
+
return "Meta"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def grounding_level(self) -> str:
|
|
111
|
+
"""Return grounding level name for phi."""
|
|
112
|
+
if self.phi < 128:
|
|
113
|
+
return "Grounded"
|
|
114
|
+
elif self.phi < 256:
|
|
115
|
+
return "Transitional"
|
|
116
|
+
elif self.phi < 384:
|
|
117
|
+
return "Abstract"
|
|
118
|
+
else:
|
|
119
|
+
return "Ethereal"
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def shell_name(self) -> str:
|
|
123
|
+
"""Return shell tier name."""
|
|
124
|
+
names = {0: "Hot", 1: "Warm", 2: "Cold", 3: "Frozen"}
|
|
125
|
+
return names[self.shell]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def encode(shell: int, theta: int, phi: int, harmonic: int) -> int:
|
|
129
|
+
"""
|
|
130
|
+
Encode RPP components into a 28-bit address.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
shell: Radial depth (0-3)
|
|
134
|
+
theta: Angular sector (0-511)
|
|
135
|
+
phi: Grounding level (0-511)
|
|
136
|
+
harmonic: Frequency/mode (0-255)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
28-bit unsigned integer
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If any component is out of range
|
|
143
|
+
"""
|
|
144
|
+
if not (0 <= shell <= MAX_SHELL):
|
|
145
|
+
raise ValueError(f"Shell must be 0-{MAX_SHELL}, got {shell}")
|
|
146
|
+
if not (0 <= theta <= MAX_THETA):
|
|
147
|
+
raise ValueError(f"Theta must be 0-{MAX_THETA}, got {theta}")
|
|
148
|
+
if not (0 <= phi <= MAX_PHI):
|
|
149
|
+
raise ValueError(f"Phi must be 0-{MAX_PHI}, got {phi}")
|
|
150
|
+
if not (0 <= harmonic <= MAX_HARMONIC):
|
|
151
|
+
raise ValueError(f"Harmonic must be 0-{MAX_HARMONIC}, got {harmonic}")
|
|
152
|
+
|
|
153
|
+
return (shell << SHELL_SHIFT) | (theta << THETA_SHIFT) | (phi << PHI_SHIFT) | harmonic
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def decode(address: int) -> Tuple[int, int, int, int]:
|
|
157
|
+
"""
|
|
158
|
+
Decode a 28-bit RPP address into components.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
address: 28-bit unsigned integer
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Tuple of (shell, theta, phi, harmonic)
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If address exceeds 28 bits
|
|
168
|
+
"""
|
|
169
|
+
if not (0 <= address <= MAX_ADDRESS):
|
|
170
|
+
raise ValueError(f"Address must be 0-{hex(MAX_ADDRESS)}, got {hex(address)}")
|
|
171
|
+
|
|
172
|
+
shell = (address >> SHELL_SHIFT) & SHELL_MASK
|
|
173
|
+
theta = (address >> THETA_SHIFT) & THETA_MASK
|
|
174
|
+
phi = (address >> PHI_SHIFT) & PHI_MASK
|
|
175
|
+
harmonic = address & HARMONIC_MASK
|
|
176
|
+
|
|
177
|
+
return (shell, theta, phi, harmonic)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def from_components(shell: int, theta: int, phi: int, harmonic: int) -> RPPAddress:
|
|
181
|
+
"""
|
|
182
|
+
Create an RPPAddress from components.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
shell: Radial depth (0-3)
|
|
186
|
+
theta: Angular sector (0-511)
|
|
187
|
+
phi: Grounding level (0-511)
|
|
188
|
+
harmonic: Frequency/mode (0-255)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
RPPAddress with encoded raw value
|
|
192
|
+
"""
|
|
193
|
+
raw = encode(shell, theta, phi, harmonic)
|
|
194
|
+
return RPPAddress(shell=shell, theta=theta, phi=phi, harmonic=harmonic, raw=raw)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def from_raw(address: int) -> RPPAddress:
|
|
198
|
+
"""
|
|
199
|
+
Create an RPPAddress from a raw 28-bit integer.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
address: 28-bit unsigned integer
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
RPPAddress with decoded components
|
|
206
|
+
"""
|
|
207
|
+
shell, theta, phi, harmonic = decode(address)
|
|
208
|
+
return RPPAddress(shell=shell, theta=theta, phi=phi, harmonic=harmonic, raw=address)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def is_valid_address(address: int) -> bool:
|
|
212
|
+
"""
|
|
213
|
+
Check if an integer is a valid 28-bit RPP address.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
address: Integer to validate
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True if valid, False otherwise
|
|
220
|
+
"""
|
|
221
|
+
return isinstance(address, int) and 0 <= address <= MAX_ADDRESS
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def parse_address(value: str) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Parse an address from string (hex or decimal).
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
value: String like "0x1234ABC" or "19141308"
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Integer address
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValueError: If parsing fails or address is invalid
|
|
236
|
+
"""
|
|
237
|
+
value = value.strip()
|
|
238
|
+
try:
|
|
239
|
+
if value.lower().startswith("0x"):
|
|
240
|
+
address = int(value, 16)
|
|
241
|
+
else:
|
|
242
|
+
address = int(value)
|
|
243
|
+
except ValueError:
|
|
244
|
+
raise ValueError(f"Cannot parse address: {value}")
|
|
245
|
+
|
|
246
|
+
if not is_valid_address(address):
|
|
247
|
+
raise ValueError(f"Address out of range: {hex(address)}")
|
|
248
|
+
|
|
249
|
+
return address
|