ogpu 0.2.0.0__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.
ogpu/__init__.py ADDED
File without changes
File without changes
ogpu/client/config.py ADDED
@@ -0,0 +1,18 @@
1
+ import json
2
+ import os
3
+
4
+ from dotenv import load_dotenv
5
+ from web3 import Web3
6
+
7
+ load_dotenv()
8
+
9
+ WEB3_RPC_URL = os.getenv("WEB3_RPC_URL")
10
+ if not WEB3_RPC_URL:
11
+ raise ValueError("`WEB3_RPC_URL` environment variable is not set.")
12
+
13
+ CLIENT_PRIVATE_KEY = os.getenv("CLIENT_PRIVATE_KEY")
14
+
15
+ # Web3 instance
16
+ WEB3 = Web3(Web3.HTTPProvider(WEB3_RPC_URL))
17
+ if not WEB3.is_connected():
18
+ raise ConnectionError("Failed to connect to the node at the provided RPC URL.")
@@ -0,0 +1,18 @@
1
+ import json
2
+
3
+ from .utils import load_contract
4
+
5
+ with open("abis/NexusAbi.json") as f:
6
+ NEXUS_ABI = json.load(f)
7
+ with open("abis/ControllerAbi.json") as f:
8
+ CONTROLLER_ABI = json.load(f)
9
+
10
+ NEXUS_CONTRACT_ADDRESS = "0x731329542F2dABC433f84BF0498791999D03FfBE"
11
+ CONTROLLER_CONTRACT_ADDRESS = "0xeb45d738E8c59BFa30bf6109340ccbb41469eedA"
12
+
13
+
14
+ NexusContract = load_contract(NEXUS_CONTRACT_ADDRESS, NEXUS_ABI)
15
+ ControllerContract = load_contract(CONTROLLER_CONTRACT_ADDRESS, CONTROLLER_ABI)
16
+
17
+ # Export the contracts for easy access
18
+ __all__ = ["NexusContract", "ControllerContract"]
ogpu/client/source.py ADDED
@@ -0,0 +1,33 @@
1
+ from eth_account import Account
2
+ from web3 import Web3
3
+
4
+ from .config import CLIENT_PRIVATE_KEY
5
+ from .contracts import *
6
+ from .types import SourceParams
7
+
8
+
9
+ def create_source(
10
+ web3: Web3,
11
+ source_params: SourceParams,
12
+ private_key: str | None = CLIENT_PRIVATE_KEY,
13
+ ) -> str:
14
+ acc = Account.from_key(private_key)
15
+
16
+ tx = NexusContract.functions.publishSource(
17
+ source_params.to_tuple()
18
+ ).build_transaction(
19
+ {
20
+ "from": acc.address,
21
+ "value": web3.to_wei(source_params.minPayment, "wei"),
22
+ "nonce": web3.eth.get_transaction_count(acc.address),
23
+ "gas": 8000000,
24
+ "gasPrice": web3.to_wei("10", "gwei"),
25
+ }
26
+ )
27
+
28
+ signed = web3.eth.account.sign_transaction(tx, private_key)
29
+ tx_hash = web3.eth.send_raw_transaction(signed.rawTransaction)
30
+ receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
31
+
32
+ logs = NexusContract.events.SourcePublished().process_receipt(receipt)
33
+ return logs[0]["args"]["source"]
ogpu/client/types.py ADDED
@@ -0,0 +1,46 @@
1
+ import time
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class SourceParams(BaseModel):
8
+ clientAddress: str
9
+ imageMetadataUrl: str
10
+ imageEnvironments: bytes
11
+ minPayment: int
12
+ minAvailableLockup: int
13
+ maxExpiryTime: int
14
+ privacyEnabled: bool
15
+ optionalParamsUrl: str
16
+ deliveryMethod: int
17
+ lastUpdateTime: int = int(time.time())
18
+
19
+ def to_tuple(self):
20
+ return (
21
+ self.clientAddress,
22
+ self.imageMetadataUrl,
23
+ self.imageEnvironments,
24
+ self.minPayment,
25
+ self.minAvailableLockup,
26
+ self.maxExpiryTime,
27
+ self.privacyEnabled,
28
+ self.optionalParamsUrl,
29
+ self.deliveryMethod,
30
+ self.lastUpdateTime,
31
+ )
32
+
33
+
34
+ class TaskParams(BaseModel):
35
+ source: str
36
+ config: str
37
+ expiryTime: int
38
+ payment: int
39
+
40
+ def to_tuple(self):
41
+ return (
42
+ self.source,
43
+ self.config,
44
+ self.expiryTime,
45
+ self.payment,
46
+ )
ogpu/client/utils.py ADDED
@@ -0,0 +1,9 @@
1
+ from typing import Dict
2
+
3
+ from web3 import Web3
4
+
5
+ from .config import WEB3
6
+
7
+
8
+ def load_contract(address: str, abi: Dict):
9
+ return WEB3.eth.contract(address=Web3.to_checksum_address(address), abi=abi)
@@ -0,0 +1,7 @@
1
+ """
2
+ Basic imports for registering, serving, and logging user-defined functions.
3
+ """
4
+
5
+ from .decorators import expose # Function registration decorator
6
+ from .logger import logger # Logging interface
7
+ from .server import start # Server launcher
ogpu/service/config.py ADDED
@@ -0,0 +1,8 @@
1
+ import os
2
+
3
+ SENTRY_DSN = os.getenv("OGPU_SERVICE_SENTRY_DSN")
4
+
5
+ SOURCE_ADDRESS = os.getenv("SOURCE_ADDRESS", "0xundefined")
6
+
7
+ SERVICE_HOST = "0.0.0.0"
8
+ SERVICE_PORT = 5555
@@ -0,0 +1,94 @@
1
+ import inspect
2
+ import threading
3
+ from functools import wraps
4
+ from typing import get_type_hints
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from .handler import add_handler, get_handlers
9
+ from .logger import logger
10
+
11
+
12
+ def expose(timeout: int | None = None):
13
+ """
14
+ Decorator to expose user functions as handlers for OpenGPU service.
15
+ The function's input and output must be Pydantic BaseModel.
16
+ An optional timeout can be set for background execution.
17
+
18
+ Args:
19
+ timeout (int, optional): Timeout duration in seconds. If set, the handler will return None if not completed within this time.
20
+ """
21
+
22
+ def decorator(func):
23
+ function_name = func.__name__
24
+ # Check for unique handler names
25
+ existing_names = [f.__name__ for f, _, _ in get_handlers()]
26
+ if function_name in existing_names:
27
+ raise ValueError(
28
+ f"A handler named `{function_name}` is already registered."
29
+ )
30
+
31
+ sig = inspect.signature(func)
32
+ parameters = list(sig.parameters.values())
33
+ if len(parameters) != 1:
34
+ raise TypeError(
35
+ f"Function `{function_name}` must take exactly ONE input argument (got {len(parameters)})"
36
+ )
37
+
38
+ hints = get_type_hints(func)
39
+ if "return" not in hints:
40
+ raise TypeError(f"Function `{function_name}` must have a return type.")
41
+
42
+ input_model = hints[parameters[0].name]
43
+ output_model = hints["return"]
44
+
45
+ # Check if input and output types are subclasses of Pydantic BaseModel
46
+ if not (inspect.isclass(input_model) and issubclass(input_model, BaseModel)):
47
+ raise TypeError(
48
+ f"Input to `{function_name}` must be a subclass of pydantic.BaseModel"
49
+ )
50
+
51
+ if not (inspect.isclass(output_model) and issubclass(output_model, BaseModel)):
52
+ raise TypeError(
53
+ f"Return type of `{function_name}` must be a subclass of pydantic.BaseModel"
54
+ )
55
+
56
+ if timeout:
57
+
58
+ @wraps(func)
59
+ def timed_handler(data):
60
+ """
61
+ Handler that runs with a timeout. Returns None if not completed in time.
62
+ """
63
+ result = [None]
64
+ done = threading.Event()
65
+
66
+ def run():
67
+ try:
68
+ result[0] = func(data)
69
+ except Exception as e:
70
+ logger.task_fail(f"Exception in `{function_name}`: {e}") # type: ignore
71
+ finally:
72
+ done.set()
73
+
74
+ thread = threading.Thread(target=run)
75
+ thread.start()
76
+ finished = done.wait(timeout)
77
+
78
+ if not finished:
79
+ logger.task_timeout( # type: ignore
80
+ f"`{function_name}` timed out after {timeout} seconds"
81
+ )
82
+ # Do not wait for the result, just return None immediately
83
+ return None
84
+
85
+ return result[0]
86
+
87
+ add_handler(timed_handler, input_model, output_model)
88
+ return func
89
+
90
+ # If no timeout, add handler directly
91
+ add_handler(func, input_model, output_model)
92
+ return func
93
+
94
+ return decorator
@@ -0,0 +1,31 @@
1
+ from typing import Callable, List, Tuple, Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+ # List of registered handler functions: (function, input model, output model)
6
+ _exposed_handlers: List[Tuple[Callable, Type[BaseModel], Type[BaseModel]]] = []
7
+
8
+
9
+ def add_handler(
10
+ fn: Callable, input_model: Type[BaseModel], output_model: Type[BaseModel]
11
+ ):
12
+ """
13
+ Registers a new handler function with its input and output models.
14
+
15
+ Args:
16
+ fn (Callable): The handler function to register.
17
+ input_model (Type[BaseModel]): The Pydantic model for input validation.
18
+ output_model (Type[BaseModel]): The Pydantic model for output validation.
19
+ """
20
+ _exposed_handlers.append((fn, input_model, output_model))
21
+
22
+
23
+ def get_handlers():
24
+ """
25
+ Returns all registered handler functions.
26
+
27
+ Returns:
28
+ List[Tuple[Callable, Type[BaseModel], Type[BaseModel]]]:
29
+ A list of tuples containing the handler function, input model, and output model.
30
+ """
31
+ return _exposed_handlers
ogpu/service/logger.py ADDED
@@ -0,0 +1,102 @@
1
+ import logging
2
+ import os
3
+
4
+ import sentry_sdk
5
+ from colorama import Fore, Style, init
6
+
7
+ from . import config
8
+
9
+ init(autoreset=True)
10
+
11
+ logger = logging.getLogger("ogpu")
12
+
13
+
14
+ if not logger.hasHandlers():
15
+ handler = logging.StreamHandler()
16
+
17
+ class PartyFormatter(logging.Formatter):
18
+ """
19
+ Custom formatter that colors log messages by level.
20
+ """
21
+
22
+ def format(self, record):
23
+ themes = {
24
+ "DEBUG": (Fore.BLUE, "DEBUG"),
25
+ "INFO": (Fore.GREEN, "INFO "),
26
+ "SUCCESS": (Fore.CYAN, "โœ… SUCCESS"),
27
+ "WARNING": (Fore.YELLOW, "WARNING"),
28
+ "ERROR": (Fore.RED, "ERROR"),
29
+ "FAIL": (Fore.RED, "โŒ FAIL"),
30
+ "TIMEOUT": (Fore.LIGHTMAGENTA_EX, "โฑ๏ธ TIMEOUT"),
31
+ "CRITICAL": (Fore.MAGENTA, "CRITICAL"),
32
+ }
33
+ color, label = themes.get(record.levelname, ("", record.levelname))
34
+ record.levelname = f"{color}{label}{Style.RESET_ALL}"
35
+ return super().format(record)
36
+
37
+ formatter = PartyFormatter(
38
+ fmt="[{asctime}] {levelname:<20} {message}",
39
+ datefmt="%Y-%m-%d %H:%M:%S",
40
+ style="{",
41
+ )
42
+ handler.setFormatter(formatter)
43
+ logger.addHandler(handler)
44
+
45
+ logger.setLevel(logging.INFO)
46
+
47
+ # Define custom log levels
48
+ SUCCESS_LEVEL = 25
49
+ TIMEOUT_LEVEL = 35
50
+ FAIL_LEVEL = 45
51
+
52
+ logging.addLevelName(SUCCESS_LEVEL, "SUCCESS")
53
+ logging.addLevelName(FAIL_LEVEL, "FAIL")
54
+ logging.addLevelName(TIMEOUT_LEVEL, "TIMEOUT")
55
+
56
+
57
+ def task_success(self, message, *args, **kwargs):
58
+ """Custom log level for successful operations."""
59
+ if self.isEnabledFor(SUCCESS_LEVEL):
60
+ self._log(SUCCESS_LEVEL, message, args, **kwargs)
61
+
62
+
63
+ def task_fail(self, message, *args, **kwargs):
64
+ """Custom log level for failed operations."""
65
+ if self.isEnabledFor(FAIL_LEVEL):
66
+ self._log(FAIL_LEVEL, message, args, **kwargs)
67
+ if config.SENTRY_DSN:
68
+ try:
69
+ sentry_sdk.capture_message(str(message), level="error")
70
+ except Exception:
71
+ pass
72
+
73
+
74
+ def task_timeout(self, message, *args, **kwargs):
75
+ """Custom log level for timeout situations."""
76
+ if self.isEnabledFor(TIMEOUT_LEVEL):
77
+ self._log(TIMEOUT_LEVEL, message, args, **kwargs)
78
+ if config.SENTRY_DSN:
79
+ try:
80
+ sentry_sdk.capture_message(str(message), level="warning")
81
+ except Exception:
82
+ pass
83
+
84
+
85
+ # Add custom levels to Logger
86
+ setattr(logging.Logger, "task_success", task_success)
87
+ setattr(logging.Logger, "task_fail", task_fail)
88
+ setattr(logging.Logger, "task_timeout", task_timeout)
89
+
90
+
91
+ if config.SENTRY_DSN:
92
+ sentry_sdk.init(
93
+ dsn=config.SENTRY_DSN,
94
+ traces_sample_rate=0.1,
95
+ environment=os.getenv("OGPU_SERVICE_ENV", "production"),
96
+ release="0.0.1",
97
+ )
98
+
99
+ with sentry_sdk.configure_scope() as scope:
100
+ scope.set_tag("source_address", config.SOURCE_ADDRESS)
101
+
102
+ logger.info("Sentry logging enabled.")
ogpu/service/server.py ADDED
@@ -0,0 +1,62 @@
1
+ import uvicorn
2
+ from fastapi import BackgroundTasks, FastAPI
3
+
4
+ from .config import SERVICE_HOST, SERVICE_PORT
5
+ from .handler import get_handlers
6
+ from .logger import logger
7
+
8
+
9
+ def start():
10
+ """
11
+ Serves registered handler functions as HTTP endpoints using FastAPI.
12
+ Creates a /run/{function}/{task_address} endpoint for each handler.
13
+ """
14
+ logger.info("Starting OpenGPU Service server...")
15
+ app = FastAPI(title="OpenGPU Service", version="0.1.0")
16
+
17
+ def create_endpoint(handler, input_model, function_name):
18
+ """
19
+ Dynamically generates an endpoint function for each handler.
20
+ """
21
+
22
+ async def endpoint(
23
+ task_address: str, data: input_model, background_tasks: BackgroundTasks # type: ignore
24
+ ):
25
+ """
26
+ Runs the handler in the background when an HTTP request is received.
27
+ """
28
+
29
+ def runner():
30
+ try:
31
+ result = handler(data)
32
+ if result:
33
+ logger.task_success( # type: ignore
34
+ f"[{task_address}] Function: `{function_name}`, Result โ†’ "
35
+ + ", ".join(
36
+ [f"{k}={v}" for k, v in result.model_dump().items()]
37
+ )
38
+ )
39
+ except Exception as e:
40
+ logger.task_fail( # type: ignore
41
+ f"[{task_address}] Error in `{function_name}`: {e}"
42
+ )
43
+
44
+ background_tasks.add_task(runner)
45
+ return {"task_address": task_address, "status": "accepted"}
46
+
47
+ return endpoint
48
+
49
+ # Create endpoints for all registered handlers
50
+ for handler, input_model, _output_model in get_handlers():
51
+ function_name = handler.__name__
52
+ path = f"/run/{function_name}/{{task_address}}"
53
+
54
+ endpoint = create_endpoint(handler, input_model, function_name)
55
+ app.post(path, status_code=202)(endpoint)
56
+ logger.info(f"Registered endpoint โ†’ /run/{function_name}/{{task_address}}")
57
+
58
+ logger.info("Connected to OpenGPU Service ๐Ÿ”ต")
59
+ logger.info("Listening on http://0.0.0.0:5555")
60
+
61
+ # Start FastAPI server
62
+ uvicorn.run(app, host=SERVICE_HOST, port=SERVICE_PORT, log_level="warning")
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: ogpu
3
+ Version: 0.2.0.0
4
+ Summary: OpenGPU SDK for distributed AI task deployment
5
+ Author-email: Kutay <kutay@opengpu.network>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: colorama==0.4.6
13
+ Requires-Dist: fastapi==0.115.12
14
+ Requires-Dist: pydantic==2.11.4
15
+ Requires-Dist: uvicorn==0.34.2
16
+ Requires-Dist: sentry_sdk==2.29.1
17
+ Requires-Dist: python-dotenv==1.1.0
18
+ Requires-Dist: web3==7.12.0
19
+
20
+ # ๐Ÿง  OpenGPU SDK
21
+
22
+ Welcome to the edge of distributed AI.
23
+
24
+ The `ogpu.service` SDK lets you write **task handlers** that will run **on remote provider machines** โ€” not your laptop.
25
+ You define what your task expects and yapar, and we handle the wiring, serving, and background magic.
26
+
27
+ > โœจ Write your task. ๐Ÿ›ฐ๏ธ Deploy it. โšก๏ธ Let the network run it.
28
+
29
+ ---
30
+
31
+ ## ๐Ÿš€ What is this?
32
+
33
+ This SDK is used by **client developers** to write Python **tasks** (as functions) that will be deployed and executed **on OpenGPU network providers**.
34
+
35
+ Your code will:
36
+ - Accept inputs (Pydantic model)
37
+ - Process them inside a registered **task handler**
38
+ - Expose a `/run/{task}/{task_address}` endpoint via FastAPI
39
+ - Be served by remote compute
40
+
41
+ ---
42
+
43
+ ## ๐Ÿงช Example: Your First Task
44
+
45
+ ```python
46
+ import ogpu.service
47
+ from pydantic import BaseModel
48
+
49
+ class MultiplyInput(BaseModel):
50
+ a: int
51
+ b: int
52
+
53
+ class MultiplyOutput(BaseModel):
54
+ result: int
55
+
56
+ @ogpu.service.expose()
57
+ def multiply(data: MultiplyInput) -> MultiplyOutput:
58
+ ogpu.service.logger.info(f"๐Ÿงฎ Starting multiplication: {data.a} * {data.b}")
59
+ result = data.a * data.b
60
+ ogpu.service.logger.info(f"โœ… Result computed: {result}")
61
+ return MultiplyOutput(result=result)
62
+
63
+ ogpu.service.start()
64
+ ```
65
+
66
+ Thatโ€™s it.
67
+ This exposes an endpoint at:
68
+
69
+ ```
70
+ POST /run/multiply/{task_address}
71
+ ```
72
+
73
+ With body:
74
+ ```json
75
+ {
76
+ "a": 5,
77
+ "b": 7
78
+ }
79
+ ```
80
+
81
+ ---
82
+
83
+ ## ๐Ÿ“ก How It Works
84
+
85
+ - `@expose()`: Marks your function as a **task handler**.
86
+ - `start()`: Starts a FastAPI server that awaits tasks.
87
+ - Your task runs in a background thread.
88
+ - The result is logged, not returned over HTTP.
89
+
90
+ ---
91
+
92
+ ## ๐Ÿง™ Guidelines
93
+
94
+ - Your task handler must accept **one** `pydantic.BaseModel` input
95
+ - It must return another `pydantic.BaseModel`
96
+ - Task (function) names must be **unique**
97
+ - Output will be logged to the console โ€” keep it clean ๐Ÿ’…
98
+
99
+ ---
100
+
101
+ ## ๐Ÿค Made for the OpenGPU Network
102
+ Unleash your code. Let the grid handle the rest.
@@ -0,0 +1,17 @@
1
+ ogpu/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ogpu/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ogpu/client/config.py,sha256=-sn8O25kGG6RGrd2nDdFIWcY9v54ev4ZZ4RnN2cpJrs,454
4
+ ogpu/client/contracts.py,sha256=KaMIOmQs9P4oFw12ZI8HHvcVLkPx5LMgnkpcvld8KZE,574
5
+ ogpu/client/source.py,sha256=wFHIyBIkN9QKL6FLVoyk80dKIZt01O12PcVRx8s1RZQ,1005
6
+ ogpu/client/types.py,sha256=zsin96RoM1JG-ILjhQem95vbtgz9f-dNJHL8n4FjDZA,1013
7
+ ogpu/client/utils.py,sha256=a8WBolSyeEYxl-ReybApyuo4CpzMshm2jrnu_sbdsvM,200
8
+ ogpu/service/__init__.py,sha256=7hRMGk5cxwffLxoUpZHoGIRVCrS3EaEZClTLmetB_H8,244
9
+ ogpu/service/config.py,sha256=3wDxVXDmiMcCYi8o5yxunTYuIpVSWMhnJwNCu7t72dI,168
10
+ ogpu/service/decorators.py,sha256=d3daBHQIOb4FxwJo2xwTwz8q-IAuuMfxeSarZV61oeU,3268
11
+ ogpu/service/handler.py,sha256=niqnTKRbYhVB5l-UbjNUh_lXggUXAmuuRySwlnO0jSc,1002
12
+ ogpu/service/logger.py,sha256=Tjr9r9Y_l61v5t--A4mVAyFIZ-dlfLZbvH-DkQa-bTA,2997
13
+ ogpu/service/server.py,sha256=jtKP8FPPtlbgggUikhVcOxu4bwh18_sxCFDhQOrgll0,2292
14
+ ogpu-0.2.0.0.dist-info/METADATA,sha256=EV7rBuwmE27UaxflWADQOaYRkGKfqghylhhKDWABlR8,2560
15
+ ogpu-0.2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ ogpu-0.2.0.0.dist-info/top_level.txt,sha256=LhvIzChTfyKc02C9U5fM9Uo2-EIo2oSk4kRn8JCwn9M,5
17
+ ogpu-0.2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ogpu