Skip to content

Feature: ipv4 and missing service #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ec8e14b
Feature: base client service
1yam Jun 13, 2025
16d5c43
Feature: CRN service for client
1yam Jun 13, 2025
4676c39
Feature: DNS service for client
1yam Jun 13, 2025
283b6df
Feature: Scheduler service for client
1yam Jun 13, 2025
20c6dab
Feature: Port_forwarder service for client
1yam Jun 13, 2025
ac3e7a3
Feature: new settings `DNS_API` `CRN_URL_UPDATE` `CRN_LIST_URL` `CRN_…
1yam Jun 13, 2025
b072b3a
feature: new Exception for custom service
1yam Jun 13, 2025
f6da369
fix: parse_obj is deprecated need to use model_validate
1yam Jun 13, 2025
2640abf
feat: utils func sanitize_url
1yam Jun 13, 2025
cccfbfb
feature: Utils client service
1yam Jun 13, 2025
c3978bc
fix: lint issue http_port_forwarder.py
1yam Jun 13, 2025
dcba7ee
feature: Services types
1yam Jun 13, 2025
76ff159
Feature: AlephHttpClient load default service and allow new method `r…
1yam Jun 13, 2025
d1d931b
Feature: AuthenticatedAlephHttpClient load default service and allow …
1yam Jun 13, 2025
2fc0931
feat: new unit test for client services
1yam Jun 13, 2025
b45816d
fix: import
1yam Jun 13, 2025
87b23a8
fix: domains service not existing yet
1yam Jun 13, 2025
754759f
fix: unit test
1yam Jun 13, 2025
9667106
fix: remove domains for units test service
1yam Jun 13, 2025
b4eb1d2
refactor: No __init__ needed
1yam Jun 16, 2025
f2c5a8f
Refactor: renaming class / change folder struct
1yam Jun 16, 2025
0a7bd47
fix: port forwarder import
1yam Jun 16, 2025
475e60f
fix: linting format import
1yam Jun 16, 2025
4fb796d
Feature: client.dns.get_public_dns_by_host
1yam Jun 18, 2025
7a1c107
fix: this functions not used / wrong place
1yam Jun 18, 2025
750a1d2
fix: linting FMT issue
1yam Jun 18, 2025
36532f5
feat: use new filter on dns api ?item_hash=
1yam Jun 25, 2025
4b4ebe9
fix: get_scheduler_node become get_nodes since we already on client.s…
1yam Jun 26, 2025
64d91f1
fix: rename get_ports to get_address_ports, get_port to get_ports
1yam Jun 26, 2025
9649260
fix: we should also ensure that the mlessage is not being removed whe…
1yam Jun 26, 2025
a275c89
fix: new unit test, some name change
1yam Jun 26, 2025
1561982
fix: remove unit test for now will fix them
1yam Jun 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/aleph/sdk/chains/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def from_crypto_host(
session = aiohttp.ClientSession(connector=connector)

async with session.get(f"{host}/properties") as response:
await response.raise_for_status()
response.raise_for_status()
data = await response.json()
properties = AccountProperties(**data)

Expand All @@ -75,7 +75,7 @@ def private_key(self):
async def sign_message(self, message: Dict) -> Dict:
"""Sign a message inplace."""
async with self._session.post(f"{self._host}/sign", json=message) as response:
await response.raise_for_status()
response.raise_for_status()
return await response.json()

async def sign_raw(self, buffer: bytes) -> bytes:
Expand Down
10 changes: 9 additions & 1 deletion src/aleph/sdk/client/authenticated_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ..utils import extended_json_encoder, make_instance_content, make_program_content
from .abstract import AuthenticatedAlephClient
from .http import AlephHttpClient
from .services.authenticated_port_forwarder import AuthenticatedPortForwarder

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,6 +82,13 @@ def __init__(
)
self.account = account

async def __aenter__(self):
await super().__aenter__()
# Override services with authenticated versions
self.port_forwarder = AuthenticatedPortForwarder(self)

return self

async def ipfs_push(self, content: Mapping) -> str:
"""
Push arbitrary content as JSON to the IPFS service.
Expand Down Expand Up @@ -392,7 +400,7 @@ async def create_store(
if extra_fields is not None:
values.update(extra_fields)

content = StoreContent.parse_obj(values)
content = StoreContent.model_validate(values)

message, status, _ = await self.submit(
content=content.model_dump(exclude_none=True),
Expand Down
16 changes: 15 additions & 1 deletion src/aleph/sdk/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
from aleph_message.status import MessageStatus
from pydantic import ValidationError

from aleph.sdk.client.services.crn import Crn
from aleph.sdk.client.services.dns import DNS
from aleph.sdk.client.services.instance import Instance
from aleph.sdk.client.services.port_forwarder import PortForwarder
from aleph.sdk.client.services.scheduler import Scheduler

from ..conf import settings
from ..exceptions import (
FileTooLarge,
Expand Down Expand Up @@ -123,6 +129,13 @@ async def __aenter__(self):
)
)

# Initialize default services
self.dns = DNS(self)
self.port_forwarder = PortForwarder(self)
self.crn = Crn(self)
self.scheduler = Scheduler(self)
self.instance = Instance(self)

return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
Expand All @@ -139,7 +152,8 @@ async def fetch_aggregate(self, address: str, key: str) -> Dict[str, Dict]:
resp.raise_for_status()
result = await resp.json()
data = result.get("data", dict())
return data.get(key)
final_result = data.get(key)
return final_result

async def fetch_aggregates(
self, address: str, keys: Optional[Iterable[str]] = None
Expand Down
Empty file.
190 changes: 190 additions & 0 deletions src/aleph/sdk/client/services/authenticated_port_forwarder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
from typing import TYPE_CHECKING, Optional, Tuple

from aleph_message.models import AggregateMessage, ItemHash
from aleph_message.status import MessageStatus

from aleph.sdk.client.services.base import AggregateConfig
from aleph.sdk.client.services.port_forwarder import PortForwarder
from aleph.sdk.exceptions import MessageNotProcessed, NotAuthorize
from aleph.sdk.types import AllForwarders, Ports
from aleph.sdk.utils import safe_getattr

if TYPE_CHECKING:
from aleph.sdk.client.abstract import AuthenticatedAlephClient


class AuthenticatedPortForwarder(PortForwarder):
"""
Authenticated Port Forwarder services with create and update capabilities
"""

def __init__(self, client: "AuthenticatedAlephClient"):
super().__init__(client)

async def _verify_status_processed_and_ownership(
self, item_hash: ItemHash
) -> Tuple[AggregateMessage, MessageStatus]:
"""
Verify that the message is well processed (and not rejected / pending),
This also verify the ownership of the message
"""
message: AggregateMessage
status: MessageStatus
message, status = await self._client.get_message(
item_hash=item_hash,
with_status=True,
)

# We ensure message is not Rejected (Might not be processed yet)
if status not in [MessageStatus.PROCESSED, MessageStatus.PENDING]:
raise MessageNotProcessed(item_hash=item_hash, status=status)

message_content = safe_getattr(message, "content")
address = safe_getattr(message_content, "address")

if (
not hasattr(self._client, "account")
or address != self._client.account.get_address()
):
current_address = (
self._client.account.get_address()
if hasattr(self._client, "account")
else "unknown"
)
raise NotAuthorize(
item_hash=item_hash,
target_address=address,
current_address=current_address,
)
return message, status

async def get_address_ports(
self, address: Optional[str] = None
) -> AggregateConfig[AllForwarders]:
"""
Get all port forwarding configurations for an address

Args:
address: The address to fetch configurations for.
If None, uses the authenticated client's account address.

Returns:
Port forwarding configurations
"""
if address is None:
if not hasattr(self._client, "account") or not self._client.account:
raise ValueError("No account provided and client is not authenticated")
address = self._client.account.get_address()

return await super().get_address_ports(address=address)

async def get_ports(
self, item_hash: ItemHash = None, address: Optional[str] = None
) -> Optional[Ports]:
"""
Get port forwarding configuration for a specific item hash

Args:
address: The address to fetch configurations for.
If None, uses the authenticated client's account address.
item_hash: The hash of the item to get configuration for

Returns:
Port configuration if found, otherwise empty Ports object
"""
if address is None:
if not hasattr(self._client, "account") or not self._client.account:
raise ValueError("No account provided and client is not authenticated")
address = self._client.account.get_address()

if item_hash is None:
raise ValueError("item_hash must be provided")

return await super().get_ports(address=address, item_hash=item_hash)

async def create_ports(
self, item_hash: ItemHash, ports: Ports
) -> Tuple[AggregateMessage, MessageStatus]:
"""
Create a new port forwarding configuration for an item hash

Args:
item_hash: The hash of the item (instance/program/IPFS website)
ports: Dictionary mapping port numbers to PortFlags

Returns:
Dictionary with the result of the operation
"""
if not hasattr(self._client, "account") or not self._client.account:
raise ValueError("An account is required for this operation")

# Pre Check
# _, _ = await self._verify_status_processed_and_ownership(item_hash=item_hash)

content = {str(item_hash): ports.model_dump()}

# Check if create_aggregate exists on the client
return await self._client.create_aggregate( # type: ignore
key=self.aggregate_key, content=content
)

async def update_ports(
self, item_hash: ItemHash, ports: Ports
) -> Tuple[AggregateMessage, MessageStatus]:
"""
Update an existing port forwarding configuration for an item hash

Args:
item_hash: The hash of the item (instance/program/IPFS website)
ports: Dictionary mapping port numbers to PortFlags

Returns:
Dictionary with the result of the operation
"""
if not hasattr(self._client, "account") or not self._client.account:
raise ValueError("An account is required for this operation")

# Pre Check
# _, _ = await self._verify_status_processed_and_ownership(item_hash=item_hash)

content = {}

content[str(item_hash)] = ports.model_dump()

message, status = await self._client.create_aggregate( # type: ignore
key=self.aggregate_key, content=content
)

return message, status

async def delete_ports(
self, item_hash: ItemHash
) -> Tuple[AggregateMessage, MessageStatus]:
"""
Delete port forwarding configuration for an item hash

Args:
item_hash: The hash of the item (instance/program/IPFS website) to delete configuration for

Returns:
Dictionary with the result of the operation
"""
if not hasattr(self._client, "account") or not self._client.account:
raise ValueError("An account is required for this operation")

# Pre Check
# _, _ = await self._verify_status_processed_and_ownership(item_hash=item_hash)

# Get the Port Config of the item_hash
port: Optional[Ports] = await self.get_ports(item_hash=item_hash)
if not port:
raise

content = {}
content[str(item_hash)] = port.model_dump()

# Create a new aggregate with the updated content
message, status = await self._client.create_aggregate( # type: ignore
key=self.aggregate_key, content=content
)
return message, status
42 changes: 42 additions & 0 deletions src/aleph/sdk/client/services/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from abc import ABC
from typing import TYPE_CHECKING, Generic, List, Optional, Type, TypeVar

from pydantic import BaseModel

if TYPE_CHECKING:
from aleph.sdk.client.http import AlephHttpClient


T = TypeVar("T", bound=BaseModel)


class AggregateConfig(BaseModel, Generic[T]):
"""
A generic container for "aggregate" data of type T.
- `data` will be either None or a list of T-instances.
"""

data: Optional[List[T]] = None


class BaseService(ABC, Generic[T]):
aggregate_key: str
model_cls: Type[T]

def __init__(self, client: "AlephHttpClient"):
self._client = client
self.model_cls: Type[T]

async def get_config(self, address: str):

aggregate_data = await self._client.fetch_aggregate(
address=address, key=self.aggregate_key
)

if aggregate_data:
model_instance = self.model_cls.model_validate(aggregate_data)
config = AggregateConfig[T](data=[model_instance])
else:
config = AggregateConfig[T](data=None)

return config
Loading
Loading