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.
Files changed (38) hide show
  1. altcodepro_polydb_python-2.2.4.dist-info/METADATA +489 -0
  2. altcodepro_polydb_python-2.2.4.dist-info/RECORD +57 -0
  3. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/WHEEL +1 -1
  4. polydb/__init__.py +2 -2
  5. polydb/adapters/AzureBlobStorageAdapter.py +146 -41
  6. polydb/adapters/AzureFileStorageAdapter.py +148 -43
  7. polydb/adapters/AzureQueueAdapter.py +96 -34
  8. polydb/adapters/AzureTableStorageAdapter.py +462 -119
  9. polydb/adapters/BlockchainBlobAdapter.py +111 -0
  10. polydb/adapters/BlockchainKVAdapter.py +152 -0
  11. polydb/adapters/BlockchainQueueAdapter.py +116 -0
  12. polydb/adapters/DynamoDBAdapter.py +463 -176
  13. polydb/adapters/FirestoreAdapter.py +320 -148
  14. polydb/adapters/GCPPubSubAdapter.py +217 -0
  15. polydb/adapters/GCPStorageAdapter.py +184 -39
  16. polydb/adapters/MongoDBAdapter.py +159 -39
  17. polydb/adapters/PostgreSQLAdapter.py +285 -83
  18. polydb/adapters/S3Adapter.py +172 -35
  19. polydb/adapters/S3CompatibleAdapter.py +62 -8
  20. polydb/adapters/SQSAdapter.py +121 -44
  21. polydb/adapters/VercelBlobAdapter.py +196 -0
  22. polydb/adapters/VercelKVAdapter.py +275 -283
  23. polydb/adapters/VercelQueueAdapter.py +61 -0
  24. polydb/audit/AuditStorage.py +1 -1
  25. polydb/base/NoSQLKVAdapter.py +113 -101
  26. polydb/base/ObjectStorageAdapter.py +42 -6
  27. polydb/base/QueueAdapter.py +2 -2
  28. polydb/base/SharedFilesAdapter.py +2 -2
  29. polydb/cloudDatabaseFactory.py +200 -0
  30. polydb/databaseFactory.py +434 -101
  31. polydb/models.py +63 -1
  32. polydb/query.py +111 -42
  33. altcodepro_polydb_python-2.2.2.dist-info/METADATA +0 -379
  34. altcodepro_polydb_python-2.2.2.dist-info/RECORD +0 -52
  35. polydb/adapters/PubSubAdapter.py +0 -85
  36. polydb/factory.py +0 -107
  37. {altcodepro_polydb_python-2.2.2.dist-info → altcodepro_polydb_python-2.2.4.dist-info}/licenses/LICENSE +0 -0
  38. {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}