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 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
+ ]
@@ -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