feedo-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: feedo-sdk
3
+ Version: 0.1.0
4
+ Summary: The official Developer SDK for Feedo Network
5
+ Author: Feedo Network
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: httpx>=0.24.0
8
+ Dynamic: author
9
+ Dynamic: requires-dist
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,4 @@
1
+ from .client import FeedoClient
2
+ from .router import NodeRouter
3
+
4
+ __all__ = ["FeedoClient", "NodeRouter"]
@@ -0,0 +1,13 @@
1
+ from typing import List, Optional
2
+ from .router import NodeRouter
3
+ from .modules.search import SearchModule
4
+ from .modules.consensus import ConsensusModule
5
+ from .modules.storage import StorageModule
6
+
7
+ class FeedoClient:
8
+ def __init__(self, search_seeds: Optional[List[str]] = None, consensus_seeds: Optional[List[str]] = None, storage_seeds: Optional[List[str]] = None):
9
+ self.router = NodeRouter(search_seeds, consensus_seeds, storage_seeds)
10
+
11
+ self.search = SearchModule(self.router)
12
+ self.consensus = ConsensusModule(self.router)
13
+ self.storage = StorageModule(self.router)
@@ -0,0 +1 @@
1
+ # Init modules
@@ -0,0 +1,46 @@
1
+ import httpx
2
+ from typing import Dict, Any, Optional
3
+ from ..router import NodeRouter
4
+
5
+ class ConsensusModule:
6
+ def __init__(self, router: NodeRouter):
7
+ self.router = router
8
+
9
+ async def _request(self, method: str, path: str, json: Optional[Dict] = None) -> Any:
10
+ base_url = await self.router.get_consensus_node()
11
+ url = f"{base_url}{path}"
12
+
13
+ async with httpx.AsyncClient() as client:
14
+ try:
15
+ response = await client.request(method, url, json=json)
16
+ response.raise_for_status()
17
+ return response.json()
18
+ except Exception:
19
+ print(f"Consensus request failed on {base_url}, finding new node...")
20
+ self.router.invalidate_consensus_node()
21
+ base_url = await self.router.get_consensus_node()
22
+ url = f"{base_url}{path}"
23
+ response = await client.request(method, url, json=json)
24
+ response.raise_for_status()
25
+ return response.json()
26
+
27
+ async def resolve_name(self, name: str):
28
+ return await self._request("GET", f"/resolve/{name}")
29
+
30
+ async def resolve_cid(self, cid: str):
31
+ return await self._request("GET", f"/resolve_cid/{cid}")
32
+
33
+ async def get_did_balance(self, did: str):
34
+ return await self._request("GET", f"/did/{did}/balance")
35
+
36
+ async def register_did(self, pubkey_hex: str, signature_hex: str):
37
+ return await self._request("POST", "/did/register", json={"pubkey_hex": pubkey_hex, "signature_hex": signature_hex})
38
+
39
+ async def register_name(self, name: str, did: str, cid: str, signature_hex: str):
40
+ return await self._request("POST", "/name/register", json={"name": name, "did": did, "cid": cid, "signature_hex": signature_hex})
41
+
42
+ async def update_name_cid(self, name: str, new_cid: str, signature_hex: str):
43
+ return await self._request("POST", "/name/update_cid", json={"name": name, "new_cid": new_cid, "signature_hex": signature_hex})
44
+
45
+ async def list_grants(self):
46
+ return await self._request("GET", "/grants")
@@ -0,0 +1,40 @@
1
+ import httpx
2
+ from typing import Dict, Any, Optional
3
+ from ..router import NodeRouter
4
+
5
+ class SearchModule:
6
+ def __init__(self, router: NodeRouter):
7
+ self.router = router
8
+
9
+ async def _request(self, method: str, path: str, json: Optional[Dict] = None, params: Optional[Dict] = None) -> Any:
10
+ base_url = await self.router.get_search_node()
11
+ url = f"{base_url}{path}"
12
+
13
+ async with httpx.AsyncClient() as client:
14
+ try:
15
+ response = await client.request(method, url, json=json, params=params)
16
+ response.raise_for_status()
17
+ return response.json()
18
+ except Exception as e:
19
+ print(f"Search request failed on {base_url}, finding new node...")
20
+ self.router.invalidate_search_node()
21
+ base_url = await self.router.get_search_node()
22
+ url = f"{base_url}{path}"
23
+ response = await client.request(method, url, json=json, params=params)
24
+ response.raise_for_status()
25
+ return response.json()
26
+
27
+ async def query(self, query_text: str, limit: int = 10):
28
+ return await self._request("GET", "/query", params={"q": query_text, "limit": limit})
29
+
30
+ async def index_document(self, content: str, metadata: Optional[Dict] = None):
31
+ return await self._request("POST", "/index_document", json={"content": content, "metadata": metadata or {}})
32
+
33
+ async def deploy_proxy(self, directory_path: str, domain: str):
34
+ return await self._request("POST", "/proxy/publish_feedo", json={"source_dir": directory_path, "domain": domain})
35
+
36
+ async def unpin(self, cid: str):
37
+ return await self._request("DELETE", f"/proxy/unpin_feedo/{cid}")
38
+
39
+ async def get_stats(self):
40
+ return await self._request("GET", "/explorer/stats")
@@ -0,0 +1,44 @@
1
+ import httpx
2
+ from typing import Dict, Any, Optional
3
+ from ..router import NodeRouter
4
+
5
+ class StorageModule:
6
+ def __init__(self, router: NodeRouter):
7
+ self.router = router
8
+
9
+ async def _request(self, method: str, path: str, json: Optional[Dict] = None, data: Any = None, files: Any = None) -> Any:
10
+ base_url = await self.router.get_storage_node()
11
+ url = f"{base_url}{path}"
12
+
13
+ async with httpx.AsyncClient() as client:
14
+ try:
15
+ response = await client.request(method, url, json=json, data=data, files=files)
16
+ response.raise_for_status()
17
+ # download endpoint might not return json
18
+ if response.headers.get("content-type") == "application/json":
19
+ return response.json()
20
+ return response.content
21
+ except Exception:
22
+ print(f"Storage request failed on {base_url}, finding new node...")
23
+ self.router.invalidate_storage_node()
24
+ base_url = await self.router.get_storage_node()
25
+ url = f"{base_url}{path}"
26
+ response = await client.request(method, url, json=json, data=data, files=files)
27
+ response.raise_for_status()
28
+ if response.headers.get("content-type") == "application/json":
29
+ return response.json()
30
+ return response.content
31
+
32
+ async def upload_file(self, file_path: str, filename: str = "file"):
33
+ with open(file_path, "rb") as f:
34
+ files = {"file": (filename, f)}
35
+ return await self._request("POST", "/upload", files=files)
36
+
37
+ async def download_file(self, hash_id: str) -> bytes:
38
+ return await self._request("GET", f"/download/{hash_id}")
39
+
40
+ async def ingest_json(self, payload: Dict):
41
+ return await self._request("POST", "/api/v1/ingest/post", json=payload)
42
+
43
+ async def get_recent_files(self):
44
+ return await self._request("GET", "/api/files/recent")
@@ -0,0 +1,65 @@
1
+ import httpx
2
+ import asyncio
3
+ from typing import List, Optional, Dict
4
+
5
+ DEFAULT_SEEDS = {
6
+ "search": ["http://localhost:8000"],
7
+ "consensus": ["http://localhost:8080"],
8
+ "storage": ["http://localhost:8081"]
9
+ }
10
+
11
+ class NodeRouter:
12
+ def __init__(self, search_seeds: Optional[List[str]] = None, consensus_seeds: Optional[List[str]] = None, storage_seeds: Optional[List[str]] = None):
13
+ self.search_nodes = search_seeds or DEFAULT_SEEDS["search"]
14
+ self.consensus_nodes = consensus_seeds or DEFAULT_SEEDS["consensus"]
15
+ self.storage_nodes = storage_seeds or DEFAULT_SEEDS["storage"]
16
+
17
+ self._active_search_node = None
18
+ self._active_consensus_node = None
19
+ self._active_storage_node = None
20
+
21
+ async def _find_fastest_node(self, nodes: List[str], health_endpoint: str) -> str:
22
+ async def ping(node: str) -> str:
23
+ async with httpx.AsyncClient() as client:
24
+ url = f"{node}{health_endpoint}"
25
+ response = await client.get(url, timeout=3.0)
26
+ response.raise_for_status()
27
+ return node
28
+
29
+ tasks = [asyncio.create_task(ping(node)) for node in nodes]
30
+
31
+ while tasks:
32
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
33
+ for task in done:
34
+ try:
35
+ return task.result()
36
+ except Exception:
37
+ pass
38
+ tasks = list(pending)
39
+
40
+ print(f"Warning: All seed nodes failed. Falling back to {nodes[0]}")
41
+ return nodes[0]
42
+
43
+ async def get_search_node(self) -> str:
44
+ if not self._active_search_node:
45
+ self._active_search_node = await self._find_fastest_node(self.search_nodes, "/explorer/stats")
46
+ return self._active_search_node
47
+
48
+ async def get_consensus_node(self) -> str:
49
+ if not self._active_consensus_node:
50
+ self._active_consensus_node = await self._find_fastest_node(self.consensus_nodes, "/grants")
51
+ return self._active_consensus_node
52
+
53
+ async def get_storage_node(self) -> str:
54
+ if not self._active_storage_node:
55
+ self._active_storage_node = await self._find_fastest_node(self.storage_nodes, "/api/files/recent")
56
+ return self._active_storage_node
57
+
58
+ def invalidate_search_node(self):
59
+ self._active_search_node = None
60
+
61
+ def invalidate_consensus_node(self):
62
+ self._active_consensus_node = None
63
+
64
+ def invalidate_storage_node(self):
65
+ self._active_storage_node = None
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: feedo-sdk
3
+ Version: 0.1.0
4
+ Summary: The official Developer SDK for Feedo Network
5
+ Author: Feedo Network
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: httpx>=0.24.0
8
+ Dynamic: author
9
+ Dynamic: requires-dist
10
+ Dynamic: requires-python
11
+ Dynamic: summary
@@ -0,0 +1,14 @@
1
+ setup.py
2
+ feedo/__init__.py
3
+ feedo/client.py
4
+ feedo/router.py
5
+ feedo/modules/__init__.py
6
+ feedo/modules/consensus.py
7
+ feedo/modules/search.py
8
+ feedo/modules/storage.py
9
+ feedo_sdk.egg-info/PKG-INFO
10
+ feedo_sdk.egg-info/SOURCES.txt
11
+ feedo_sdk.egg-info/dependency_links.txt
12
+ feedo_sdk.egg-info/requires.txt
13
+ feedo_sdk.egg-info/top_level.txt
14
+ tests/test_router.py
@@ -0,0 +1 @@
1
+ httpx>=0.24.0
@@ -0,0 +1 @@
1
+ feedo
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="feedo-sdk",
5
+ version="0.1.0",
6
+ description="The official Developer SDK for Feedo Network",
7
+ author="Feedo Network",
8
+ packages=find_packages(),
9
+ install_requires=[
10
+ "httpx>=0.24.0",
11
+ ],
12
+ python_requires=">=3.8",
13
+ )
@@ -0,0 +1,47 @@
1
+ import pytest
2
+ import respx
3
+ import httpx
4
+ import asyncio
5
+ from feedo.router import NodeRouter
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_find_fastest_node_all_up():
9
+ router = NodeRouter(search_seeds=["http://node1", "http://node2"])
10
+
11
+ with respx.mock:
12
+ # Mock node1 to respond instantly
13
+ route1 = respx.get("http://node1/explorer/stats").mock(return_value=httpx.Response(200, json={}))
14
+
15
+ # Mock node2 to be slightly delayed (not natively easy in respx without custom side effects,
16
+ # so we just let them both return and asyncio.wait handles whichever resolves first - since route1 is first,
17
+ # or we can mock one to raise a timeout and the other to succeed)
18
+
19
+ route2 = respx.get("http://node2/explorer/stats").mock(side_effect=httpx.TimeoutException("Timeout"))
20
+
21
+ active_node = await router.get_search_node()
22
+ assert active_node == "http://node1"
23
+
24
+ @pytest.mark.asyncio
25
+ async def test_fallback_when_one_down():
26
+ router = NodeRouter(search_seeds=["http://node1", "http://node2"])
27
+
28
+ with respx.mock:
29
+ # node1 fails
30
+ respx.get("http://node1/explorer/stats").mock(return_value=httpx.Response(500))
31
+ # node2 succeeds
32
+ respx.get("http://node2/explorer/stats").mock(return_value=httpx.Response(200, json={}))
33
+
34
+ active_node = await router.get_search_node()
35
+ assert active_node == "http://node2"
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_fallback_when_all_down():
39
+ router = NodeRouter(search_seeds=["http://node1", "http://node2"])
40
+
41
+ with respx.mock:
42
+ respx.get("http://node1/explorer/stats").mock(side_effect=httpx.ConnectError("Down"))
43
+ respx.get("http://node2/explorer/stats").mock(side_effect=httpx.ConnectError("Down"))
44
+
45
+ active_node = await router.get_search_node()
46
+ # Should fallback to the first node if all fail
47
+ assert active_node == "http://node1"