altcodepro-polydb-python 2.2.2__py3-none-any.whl → 2.2.4__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.
- altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
- altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
- polydb/__init__.py +2 -2
- polydb/adapters/AzureBlobStorageAdapter.py +146 -41
- polydb/adapters/AzureFileStorageAdapter.py +148 -43
- polydb/adapters/AzureQueueAdapter.py +96 -34
- polydb/adapters/AzureTableStorageAdapter.py +462 -119
- polydb/adapters/BlockchainBlobAdapter.py +111 -0
- polydb/adapters/BlockchainKVAdapter.py +152 -0
- polydb/adapters/BlockchainQueueAdapter.py +116 -0
- polydb/adapters/DynamoDBAdapter.py +463 -176
- polydb/adapters/FirestoreAdapter.py +320 -148
- polydb/adapters/GCPPubSubAdapter.py +217 -0
- polydb/adapters/GCPStorageAdapter.py +184 -39
- polydb/adapters/MongoDBAdapter.py +159 -39
- polydb/adapters/PostgreSQLAdapter.py +285 -83
- polydb/adapters/S3Adapter.py +172 -35
- polydb/adapters/S3CompatibleAdapter.py +62 -8
- polydb/adapters/SQSAdapter.py +121 -44
- polydb/adapters/VercelBlobAdapter.py +196 -0
- polydb/adapters/VercelKVAdapter.py +275 -283
- polydb/adapters/VercelQueueAdapter.py +61 -0
- polydb/audit/AuditStorage.py +1 -1
- polydb/base/NoSQLKVAdapter.py +113 -101
- polydb/base/ObjectStorageAdapter.py +42 -6
- polydb/base/QueueAdapter.py +2 -2
- polydb/base/SharedFilesAdapter.py +2 -2
- polydb/cloudDatabaseFactory.py +200 -0
- polydb/databaseFactory.py +434 -101
- polydb/models.py +63 -1
- polydb/query.py +111 -42
- altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
- altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
- polydb/adapters/PubSubAdapter.py +0 -85
- polydb/factory.py +0 -107
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import ipfshttpclient
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from ..base.ObjectStorageAdapter import ObjectStorageAdapter
|
|
10
|
+
from ..errors import StorageError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlockchainBlobAdapter(ObjectStorageAdapter):
|
|
14
|
+
"""
|
|
15
|
+
Blob storage backed by IPFS.
|
|
16
|
+
|
|
17
|
+
- key is ignored for storage (IPFS is content-addressed)
|
|
18
|
+
- returns CID (acts as key)
|
|
19
|
+
- metadata is not natively supported → ignored or embedded externally
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, ipfs_url: Optional[str] = None):
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
load_dotenv()
|
|
26
|
+
|
|
27
|
+
self.ipfs_url = ipfs_url or os.getenv("IPFS_API_URL", "/dns/localhost/tcp/5001/http")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import ipfshttpclient.client
|
|
31
|
+
|
|
32
|
+
ipfshttpclient.client.assert_version = lambda *args, **kwargs: None
|
|
33
|
+
|
|
34
|
+
self.client = ipfshttpclient.connect(self.ipfs_url, session=True)
|
|
35
|
+
self.logger.info(f"Connected to IPFS: {self.ipfs_url}")
|
|
36
|
+
|
|
37
|
+
except Exception as e:
|
|
38
|
+
raise StorageError(f"Failed to connect to IPFS: {e}")
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------
|
|
41
|
+
# PUT
|
|
42
|
+
# --------------------------------------------------
|
|
43
|
+
def _put_raw(
|
|
44
|
+
self,
|
|
45
|
+
key: str,
|
|
46
|
+
data: bytes,
|
|
47
|
+
fileName: str = "",
|
|
48
|
+
media_type: Optional[str] = None,
|
|
49
|
+
metadata: Dict[str, Any] | None = None,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Upload data to IPFS and return CID"""
|
|
52
|
+
try:
|
|
53
|
+
res = self.client.add_bytes(data) # returns CID
|
|
54
|
+
cid = res
|
|
55
|
+
|
|
56
|
+
self.logger.debug(f"IPFS upload success cid={cid}")
|
|
57
|
+
|
|
58
|
+
return cid
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise StorageError(f"IPFS put failed: {e}")
|
|
62
|
+
|
|
63
|
+
# --------------------------------------------------
|
|
64
|
+
# GET
|
|
65
|
+
# --------------------------------------------------
|
|
66
|
+
def get(self, key: str) -> bytes:
|
|
67
|
+
"""Fetch blob from IPFS using CID"""
|
|
68
|
+
try:
|
|
69
|
+
data = self.client.cat(key)
|
|
70
|
+
self.logger.debug(f"IPFS get success cid={key}")
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise StorageError(f"IPFS get failed: {e}")
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------
|
|
77
|
+
# DELETE
|
|
78
|
+
# --------------------------------------------------
|
|
79
|
+
def delete(self, key: str) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
IPFS does not support true delete.
|
|
82
|
+
We can unpin to allow GC.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
self.client.pin.rm(key)
|
|
86
|
+
self.logger.debug(f"IPFS unpinned cid={key}")
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
except Exception:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------
|
|
93
|
+
# LIST
|
|
94
|
+
# --------------------------------------------------
|
|
95
|
+
def list(self, prefix: str = "") -> List[str]:
|
|
96
|
+
"""
|
|
97
|
+
IPFS has no native listing by prefix.
|
|
98
|
+
Return pinned CIDs as approximation.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
pins = self.client.pin.ls(type="all")
|
|
102
|
+
cids = list(pins.get("Keys", {}).keys())
|
|
103
|
+
|
|
104
|
+
if prefix:
|
|
105
|
+
cids = [cid for cid in cids if cid.startswith(prefix)]
|
|
106
|
+
|
|
107
|
+
self.logger.debug(f"IPFS list returned {len(cids)} items")
|
|
108
|
+
return cids
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise StorageError(f"IPFS list failed: {e}")
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from web3 import Web3
|
|
9
|
+
from web3.middleware import ExtraDataToPOAMiddleware
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
SUPPORTED_CHAINS = {
|
|
15
|
+
"ethereum",
|
|
16
|
+
"polygon",
|
|
17
|
+
"avalanche",
|
|
18
|
+
"bnb",
|
|
19
|
+
"arbitrum",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BlockchainKVAdapter:
|
|
24
|
+
"""
|
|
25
|
+
Generic blockchain key-value adapter.
|
|
26
|
+
|
|
27
|
+
Uses a smart contract to store JSON documents keyed by ID.
|
|
28
|
+
|
|
29
|
+
Supports EVM-compatible chains:
|
|
30
|
+
- Ethereum
|
|
31
|
+
- Polygon
|
|
32
|
+
- Avalanche
|
|
33
|
+
- BNB Chain
|
|
34
|
+
- Arbitrum
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
chain: Optional[str] = None,
|
|
40
|
+
rpc_url: Optional[str] = None,
|
|
41
|
+
private_key: Optional[str] = None,
|
|
42
|
+
contract_address: Optional[str] = None,
|
|
43
|
+
contract_abi: Optional[list] = None,
|
|
44
|
+
):
|
|
45
|
+
load_dotenv()
|
|
46
|
+
|
|
47
|
+
self.chain = (chain or os.getenv("BLOCKCHAIN_CHAIN", "ethereum")).lower()
|
|
48
|
+
|
|
49
|
+
if self.chain not in SUPPORTED_CHAINS:
|
|
50
|
+
raise ValueError(f"Unsupported blockchain: {self.chain}")
|
|
51
|
+
|
|
52
|
+
self.rpc_url = rpc_url or os.getenv("BLOCKCHAIN_RPC_URL")
|
|
53
|
+
self.private_key = private_key or os.getenv("BLOCKCHAIN_PRIVATE_KEY")
|
|
54
|
+
self.contract_address = contract_address or os.getenv("BLOCKCHAIN_CONTRACT")
|
|
55
|
+
|
|
56
|
+
if not self.rpc_url:
|
|
57
|
+
raise RuntimeError("BLOCKCHAIN_RPC_URL not configured")
|
|
58
|
+
|
|
59
|
+
if not self.private_key:
|
|
60
|
+
raise RuntimeError("BLOCKCHAIN_PRIVATE_KEY not configured")
|
|
61
|
+
|
|
62
|
+
if not self.contract_address:
|
|
63
|
+
raise RuntimeError("BLOCKCHAIN_CONTRACT not configured")
|
|
64
|
+
|
|
65
|
+
self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
|
|
66
|
+
|
|
67
|
+
if self.chain in {"polygon", "avalanche", "bnb"}:
|
|
68
|
+
self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
69
|
+
|
|
70
|
+
self.account = self.w3.eth.account.from_key(self.private_key)
|
|
71
|
+
|
|
72
|
+
if contract_abi is None:
|
|
73
|
+
contract_abi = self._default_abi()
|
|
74
|
+
|
|
75
|
+
self.contract = self.w3.eth.contract(
|
|
76
|
+
address=Web3.to_checksum_address(self.contract_address),
|
|
77
|
+
abi=contract_abi,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _default_abi(self):
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
"inputs": [
|
|
84
|
+
{"internalType": "string", "name": "key", "type": "string"},
|
|
85
|
+
{"internalType": "string", "name": "value", "type": "string"},
|
|
86
|
+
],
|
|
87
|
+
"name": "put",
|
|
88
|
+
"outputs": [],
|
|
89
|
+
"stateMutability": "nonpayable",
|
|
90
|
+
"type": "function",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"inputs": [{"internalType": "string", "name": "key", "type": "string"}],
|
|
94
|
+
"name": "get",
|
|
95
|
+
"outputs": [{"internalType": "string", "name": "", "type": "string"}],
|
|
96
|
+
"stateMutability": "view",
|
|
97
|
+
"type": "function",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"inputs": [{"internalType": "string", "name": "key", "type": "string"}],
|
|
101
|
+
"name": "deleteKey",
|
|
102
|
+
"outputs": [],
|
|
103
|
+
"stateMutability": "nonpayable",
|
|
104
|
+
"type": "function",
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def _send_tx(self, fn):
|
|
109
|
+
nonce = self.w3.eth.get_transaction_count(self.account.address)
|
|
110
|
+
|
|
111
|
+
tx = fn.build_transaction(
|
|
112
|
+
{
|
|
113
|
+
"from": self.account.address,
|
|
114
|
+
"nonce": nonce,
|
|
115
|
+
"gas": 500000,
|
|
116
|
+
"gasPrice": self.w3.eth.gas_price,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
signed = self.account.sign_transaction(tx)
|
|
121
|
+
tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
122
|
+
|
|
123
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
|
|
124
|
+
return receipt
|
|
125
|
+
|
|
126
|
+
def put(self, model, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
127
|
+
key = str(data["id"])
|
|
128
|
+
payload = json.dumps(data)
|
|
129
|
+
|
|
130
|
+
fn = self.contract.functions.put(key, payload)
|
|
131
|
+
self._send_tx(fn)
|
|
132
|
+
|
|
133
|
+
return data
|
|
134
|
+
|
|
135
|
+
def get(self, model, key: str) -> Optional[Dict[str, Any]]:
|
|
136
|
+
result = self.contract.functions.get(str(key)).call()
|
|
137
|
+
|
|
138
|
+
if not result:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
return json.loads(result)
|
|
142
|
+
|
|
143
|
+
def delete(self, model, key: str):
|
|
144
|
+
fn = self.contract.functions.deleteKey(str(key))
|
|
145
|
+
self._send_tx(fn)
|
|
146
|
+
|
|
147
|
+
return {"id": key}
|
|
148
|
+
|
|
149
|
+
def query(self, model, query: Dict[str, Any], limit: Optional[int] = None):
|
|
150
|
+
raise NotImplementedError(
|
|
151
|
+
"Blockchain query requires an off-chain indexer (TheGraph / Elastic)"
|
|
152
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from web3 import Web3
|
|
9
|
+
from web3.middleware import ExtraDataToPOAMiddleware
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BlockchainQueueAdapter:
|
|
16
|
+
"""
|
|
17
|
+
Queue implementation using blockchain events.
|
|
18
|
+
|
|
19
|
+
Messages are emitted via smart contract events and read via event filters.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
rpc_url: Optional[str] = None,
|
|
25
|
+
private_key: Optional[str] = None,
|
|
26
|
+
contract_address: Optional[str] = None,
|
|
27
|
+
contract_abi: Optional[list] = None,
|
|
28
|
+
):
|
|
29
|
+
load_dotenv()
|
|
30
|
+
|
|
31
|
+
self.rpc_url = rpc_url or os.getenv("BLOCKCHAIN_RPC_URL")
|
|
32
|
+
self.private_key = private_key or os.getenv("BLOCKCHAIN_PRIVATE_KEY")
|
|
33
|
+
self.contract_address = contract_address or os.getenv("BLOCKCHAIN_QUEUE_CONTRACT")
|
|
34
|
+
|
|
35
|
+
if not self.rpc_url:
|
|
36
|
+
raise RuntimeError("BLOCKCHAIN_RPC_URL not configured")
|
|
37
|
+
|
|
38
|
+
self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
|
|
39
|
+
self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
40
|
+
|
|
41
|
+
self.account = self.w3.eth.account.from_key(self.private_key)
|
|
42
|
+
|
|
43
|
+
if contract_abi is None:
|
|
44
|
+
contract_abi = self._default_abi()
|
|
45
|
+
if self.contract_address is not None:
|
|
46
|
+
self.contract = self.w3.eth.contract(
|
|
47
|
+
address=Web3.to_checksum_address(self.contract_address),
|
|
48
|
+
abi=contract_abi,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def _default_abi(self):
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
"anonymous": False,
|
|
55
|
+
"inputs": [
|
|
56
|
+
{"indexed": False, "name": "queue", "type": "string"},
|
|
57
|
+
{"indexed": False, "name": "data", "type": "string"},
|
|
58
|
+
],
|
|
59
|
+
"name": "MessagePublished",
|
|
60
|
+
"type": "event",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"inputs": [
|
|
64
|
+
{"internalType": "string", "name": "queue", "type": "string"},
|
|
65
|
+
{"internalType": "string", "name": "data", "type": "string"},
|
|
66
|
+
],
|
|
67
|
+
"name": "publish",
|
|
68
|
+
"outputs": [],
|
|
69
|
+
"stateMutability": "nonpayable",
|
|
70
|
+
"type": "function",
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def _send_tx(self, fn):
|
|
75
|
+
nonce = self.w3.eth.get_transaction_count(self.account.address)
|
|
76
|
+
|
|
77
|
+
tx = fn.build_transaction(
|
|
78
|
+
{
|
|
79
|
+
"from": self.account.address,
|
|
80
|
+
"nonce": nonce,
|
|
81
|
+
"gas": 300000,
|
|
82
|
+
"gasPrice": self.w3.eth.gas_price,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
signed = self.account.sign_transaction(tx)
|
|
87
|
+
tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
88
|
+
|
|
89
|
+
return tx_hash.hex()
|
|
90
|
+
|
|
91
|
+
def send(self, message: Dict[str, Any], queue_name: str = "default"):
|
|
92
|
+
payload = json.dumps(message)
|
|
93
|
+
fn = self.contract.functions.publish(queue_name, payload)
|
|
94
|
+
tx_hash = self._send_tx(fn)
|
|
95
|
+
|
|
96
|
+
return {"tx": tx_hash}
|
|
97
|
+
|
|
98
|
+
def receive(self, queue_name: str = "default", from_block="latest") -> List[Dict]:
|
|
99
|
+
event_filter = self.contract.events.MessagePublished.create_filter(fromBlock=from_block)
|
|
100
|
+
|
|
101
|
+
events = event_filter.get_all_entries()
|
|
102
|
+
|
|
103
|
+
messages = []
|
|
104
|
+
|
|
105
|
+
for event in events:
|
|
106
|
+
if event.args.queue == queue_name:
|
|
107
|
+
messages.append(json.loads(event.args.data))
|
|
108
|
+
|
|
109
|
+
return messages
|
|
110
|
+
|
|
111
|
+
def delete(self, message_id):
|
|
112
|
+
"""
|
|
113
|
+
Blockchain queues cannot delete events.
|
|
114
|
+
This method exists for interface compatibility.
|
|
115
|
+
"""
|
|
116
|
+
return {"deleted": False}
|