synthgraph-sdk 0.1.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.
- synthgraph/__init__.py +34 -0
- synthgraph/client.py +69 -0
- synthgraph/config.py +23 -0
- synthgraph/experiments.py +52 -0
- synthgraph/generations.py +124 -0
- synthgraph/http.py +174 -0
- synthgraph/models/__init__.py +16 -0
- synthgraph/models/experiment.py +16 -0
- synthgraph/models/generation.py +76 -0
- synthgraph/models/project.py +15 -0
- synthgraph/models/reference.py +27 -0
- synthgraph/projects.py +40 -0
- synthgraph/serialization/__init__.py +3 -0
- synthgraph/serialization/json.py +14 -0
- synthgraph_sdk-0.1.0.dist-info/METADATA +322 -0
- synthgraph_sdk-0.1.0.dist-info/RECORD +17 -0
- synthgraph_sdk-0.1.0.dist-info/WHEEL +4 -0
synthgraph/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .client import SynthGraphClient
|
|
2
|
+
from .config import SynthGraphConfig
|
|
3
|
+
from .http import SynthGraphHTTPClient, SynthGraphHTTPError
|
|
4
|
+
from .models import (
|
|
5
|
+
AssetReference,
|
|
6
|
+
DataReference,
|
|
7
|
+
DatasetReference,
|
|
8
|
+
Experiment,
|
|
9
|
+
GenerationRun,
|
|
10
|
+
GenerationStatus,
|
|
11
|
+
Generator,
|
|
12
|
+
Project,
|
|
13
|
+
Reproducibility,
|
|
14
|
+
)
|
|
15
|
+
from .projects import ProjectsAPI
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AssetReference",
|
|
21
|
+
"DataReference",
|
|
22
|
+
"DatasetReference",
|
|
23
|
+
"Experiment",
|
|
24
|
+
"GenerationRun",
|
|
25
|
+
"GenerationStatus",
|
|
26
|
+
"Generator",
|
|
27
|
+
"Project",
|
|
28
|
+
"ProjectsAPI",
|
|
29
|
+
"Reproducibility",
|
|
30
|
+
"SynthGraphClient",
|
|
31
|
+
"SynthGraphConfig",
|
|
32
|
+
"SynthGraphHTTPClient",
|
|
33
|
+
"SynthGraphHTTPError",
|
|
34
|
+
]
|
synthgraph/client.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from types import TracebackType
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from .config import SynthGraphConfig
|
|
5
|
+
from .experiments import ExperimentsAPI
|
|
6
|
+
from .generations import GenerationsAPI
|
|
7
|
+
from .http import SynthGraphHTTPClient
|
|
8
|
+
from .projects import ProjectsAPI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SynthGraphClient:
|
|
12
|
+
"""Public client for interacting with the SynthGraph API."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str | None = None,
|
|
17
|
+
*,
|
|
18
|
+
api_url: str | None = None,
|
|
19
|
+
timeout: float | None = None,
|
|
20
|
+
config: SynthGraphConfig | None = None,
|
|
21
|
+
transport: Any | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
if config is not None:
|
|
24
|
+
if any(
|
|
25
|
+
value is not None
|
|
26
|
+
for value in (api_key, api_url, timeout)
|
|
27
|
+
):
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"config cannot be combined with api_key, api_url, "
|
|
30
|
+
"or timeout"
|
|
31
|
+
)
|
|
32
|
+
self.config = config
|
|
33
|
+
else:
|
|
34
|
+
config_kwargs: dict[str, Any] = {}
|
|
35
|
+
|
|
36
|
+
if api_key is not None:
|
|
37
|
+
config_kwargs["api_key"] = api_key
|
|
38
|
+
|
|
39
|
+
if api_url is not None:
|
|
40
|
+
config_kwargs["api_url"] = api_url
|
|
41
|
+
|
|
42
|
+
if timeout is not None:
|
|
43
|
+
config_kwargs["timeout"] = timeout
|
|
44
|
+
|
|
45
|
+
self.config = SynthGraphConfig(**config_kwargs)
|
|
46
|
+
|
|
47
|
+
self._http = SynthGraphHTTPClient(
|
|
48
|
+
self.config,
|
|
49
|
+
transport=transport,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.projects = ProjectsAPI(self._http)
|
|
53
|
+
self.experiments = ExperimentsAPI(self._http)
|
|
54
|
+
self.generations = GenerationsAPI(self._http)
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
"""Close the underlying HTTP client."""
|
|
58
|
+
self._http.close()
|
|
59
|
+
|
|
60
|
+
def __enter__(self) -> Self:
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc_value: BaseException | None,
|
|
67
|
+
traceback: TracebackType | None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.close()
|
synthgraph/config.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SynthGraphConfig(BaseModel):
|
|
5
|
+
"""Configuration for communicating with the SynthGraph backend."""
|
|
6
|
+
|
|
7
|
+
model_config = ConfigDict(frozen=True)
|
|
8
|
+
|
|
9
|
+
api_url: str = Field(
|
|
10
|
+
default="https://synthgraph.onrender.com",
|
|
11
|
+
min_length=1,
|
|
12
|
+
)
|
|
13
|
+
api_key: str | None = None
|
|
14
|
+
timeout: float = Field(
|
|
15
|
+
default=30.0,
|
|
16
|
+
gt=0,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@field_validator("api_url")
|
|
20
|
+
@classmethod
|
|
21
|
+
def normalize_api_url(cls, value: str) -> str:
|
|
22
|
+
"""Remove trailing slashes from the API URL."""
|
|
23
|
+
return value.rstrip("/")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .http import SynthGraphHTTPClient
|
|
4
|
+
from .models import Experiment
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExperimentsAPI:
|
|
8
|
+
"""API operations for SynthGraph experiments."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, http: SynthGraphHTTPClient) -> None:
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def create(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
project_id: str,
|
|
17
|
+
name: str,
|
|
18
|
+
description: str | None = None,
|
|
19
|
+
) -> Experiment:
|
|
20
|
+
"""Create a new experiment within a project."""
|
|
21
|
+
payload: dict[str, Any] = {
|
|
22
|
+
"name": name,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if description is not None:
|
|
26
|
+
payload["description"] = description
|
|
27
|
+
|
|
28
|
+
data = self._http.post(
|
|
29
|
+
f"/projects/{project_id}/experiments",
|
|
30
|
+
json=payload,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return Experiment.model_validate(data)
|
|
34
|
+
|
|
35
|
+
def get(self, experiment_id: str) -> Experiment:
|
|
36
|
+
"""Retrieve an experiment by ID."""
|
|
37
|
+
data = self._http.get(
|
|
38
|
+
f"/experiments/{experiment_id}",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return Experiment.model_validate(data)
|
|
42
|
+
|
|
43
|
+
def list(self, *, project_id: str) -> list[Experiment]:
|
|
44
|
+
"""List experiments belonging to a project."""
|
|
45
|
+
data = self._http.get_list(
|
|
46
|
+
f"/projects/{project_id}/experiments",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
Experiment.model_validate(item)
|
|
51
|
+
for item in data
|
|
52
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .http import SynthGraphHTTPClient
|
|
4
|
+
from .models import (
|
|
5
|
+
AssetReference,
|
|
6
|
+
DatasetReference,
|
|
7
|
+
GenerationRun,
|
|
8
|
+
GenerationStatus,
|
|
9
|
+
Generator,
|
|
10
|
+
Reproducibility,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GenerationsAPI:
|
|
15
|
+
"""API operations for generation runs."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, http: SynthGraphHTTPClient) -> None:
|
|
18
|
+
self._http = http
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
experiment_id: str,
|
|
24
|
+
name: str,
|
|
25
|
+
generator: Generator,
|
|
26
|
+
parameters: dict[str, Any],
|
|
27
|
+
reproducibility: Reproducibility,
|
|
28
|
+
inputs: list[str | AssetReference | DatasetReference] | None = None,
|
|
29
|
+
outputs: list[str | AssetReference | DatasetReference] | None = None,
|
|
30
|
+
) -> GenerationRun:
|
|
31
|
+
"""Create a new generation run within an experiment."""
|
|
32
|
+
payload = {
|
|
33
|
+
"name": name,
|
|
34
|
+
"generator": generator.model_dump(
|
|
35
|
+
mode="json",
|
|
36
|
+
exclude_none=True,
|
|
37
|
+
),
|
|
38
|
+
"parameters": parameters,
|
|
39
|
+
"reproducibility": reproducibility.model_dump(
|
|
40
|
+
mode="json",
|
|
41
|
+
exclude_none=True,
|
|
42
|
+
exclude_defaults=True,
|
|
43
|
+
),
|
|
44
|
+
"inputs": [
|
|
45
|
+
reference.model_dump(mode="json")
|
|
46
|
+
if isinstance(
|
|
47
|
+
reference,
|
|
48
|
+
(AssetReference, DatasetReference),
|
|
49
|
+
)
|
|
50
|
+
else {"id": reference}
|
|
51
|
+
for reference in (inputs or [])
|
|
52
|
+
],
|
|
53
|
+
"outputs": [
|
|
54
|
+
reference.model_dump(mode="json")
|
|
55
|
+
if isinstance(
|
|
56
|
+
reference,
|
|
57
|
+
(AssetReference, DatasetReference),
|
|
58
|
+
)
|
|
59
|
+
else {"id": reference}
|
|
60
|
+
for reference in (outputs or [])
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
data = self._http.post(
|
|
65
|
+
f"/experiments/{experiment_id}/generations",
|
|
66
|
+
json=payload,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return GenerationRun.model_validate(data)
|
|
70
|
+
|
|
71
|
+
def get(self, generation_id: str) -> GenerationRun:
|
|
72
|
+
"""Retrieve a generation run by ID."""
|
|
73
|
+
data = self._http.get(
|
|
74
|
+
f"/generations/{generation_id}",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return GenerationRun.model_validate(data)
|
|
78
|
+
|
|
79
|
+
def list(self, *, experiment_id: str) -> list[GenerationRun]:
|
|
80
|
+
"""List generation runs belonging to an experiment."""
|
|
81
|
+
data = self._http.get_list(
|
|
82
|
+
f"/experiments/{experiment_id}/generations",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
GenerationRun.model_validate(item)
|
|
87
|
+
for item in data
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
def start(self, generation_id: str) -> GenerationRun:
|
|
91
|
+
"""Mark a generation run as running."""
|
|
92
|
+
return self._update_status(
|
|
93
|
+
generation_id,
|
|
94
|
+
GenerationStatus.RUNNING,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def complete(self, generation_id: str) -> GenerationRun:
|
|
98
|
+
"""Mark a generation run as completed."""
|
|
99
|
+
return self._update_status(
|
|
100
|
+
generation_id,
|
|
101
|
+
GenerationStatus.COMPLETED,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def fail(self, generation_id: str) -> GenerationRun:
|
|
105
|
+
"""Mark a generation run as failed."""
|
|
106
|
+
return self._update_status(
|
|
107
|
+
generation_id,
|
|
108
|
+
GenerationStatus.FAILED,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _update_status(
|
|
112
|
+
self,
|
|
113
|
+
generation_id: str,
|
|
114
|
+
status: GenerationStatus,
|
|
115
|
+
) -> GenerationRun:
|
|
116
|
+
"""Update the lifecycle status of a generation run."""
|
|
117
|
+
data = self._http.patch(
|
|
118
|
+
f"/generations/{generation_id}",
|
|
119
|
+
json={
|
|
120
|
+
"status": status.value,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return GenerationRun.model_validate(data)
|
synthgraph/http.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from types import TracebackType
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .config import SynthGraphConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SynthGraphHTTPError(Exception):
|
|
10
|
+
"""Raised when a SynthGraph API request fails."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, status_code: int, message: str) -> None:
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.message = message
|
|
15
|
+
|
|
16
|
+
super().__init__(
|
|
17
|
+
f"SynthGraph API request failed with status "
|
|
18
|
+
f"{status_code}: {message}"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SynthGraphHTTPClient:
|
|
23
|
+
"""Low-level HTTP client for the SynthGraph API."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
config: SynthGraphConfig,
|
|
28
|
+
*,
|
|
29
|
+
transport: httpx.BaseTransport | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.config = config
|
|
32
|
+
|
|
33
|
+
headers = {
|
|
34
|
+
"Accept": "application/json",
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if config.api_key is not None:
|
|
39
|
+
headers["Authorization"] = f"Bearer {config.api_key}"
|
|
40
|
+
|
|
41
|
+
self._client = httpx.Client(
|
|
42
|
+
base_url=config.api_url.rstrip("/"),
|
|
43
|
+
headers=headers,
|
|
44
|
+
timeout=config.timeout,
|
|
45
|
+
transport=transport,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get(self, path: str) -> dict[str, Any]:
|
|
49
|
+
"""Send a GET request expecting a JSON object response."""
|
|
50
|
+
response = self._client.get(path)
|
|
51
|
+
|
|
52
|
+
return self._handle_response(response)
|
|
53
|
+
|
|
54
|
+
def get_list(self, path: str) -> list[dict[str, Any]]:
|
|
55
|
+
"""Send a GET request expecting a JSON array response."""
|
|
56
|
+
response = self._client.get(path)
|
|
57
|
+
|
|
58
|
+
return self._handle_list_response(response)
|
|
59
|
+
|
|
60
|
+
def post(
|
|
61
|
+
self,
|
|
62
|
+
path: str,
|
|
63
|
+
*,
|
|
64
|
+
json: dict[str, Any] | None = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""Send a POST request, optionally with a JSON body."""
|
|
67
|
+
if json is None:
|
|
68
|
+
response = self._client.post(path)
|
|
69
|
+
else:
|
|
70
|
+
response = self._client.post(path, json=json)
|
|
71
|
+
|
|
72
|
+
return self._handle_response(response)
|
|
73
|
+
|
|
74
|
+
def patch(
|
|
75
|
+
self,
|
|
76
|
+
path: str,
|
|
77
|
+
*,
|
|
78
|
+
json: dict[str, Any] | None = None,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
"""Send a PATCH request, optionally with a JSON body."""
|
|
81
|
+
if json is None:
|
|
82
|
+
response = self._client.patch(path)
|
|
83
|
+
else:
|
|
84
|
+
response = self._client.patch(path, json=json)
|
|
85
|
+
|
|
86
|
+
return self._handle_response(response)
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
"""Close the underlying HTTP client."""
|
|
90
|
+
self._client.close()
|
|
91
|
+
|
|
92
|
+
def __enter__(self) -> Self:
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def __exit__(
|
|
96
|
+
self,
|
|
97
|
+
exc_type: type[BaseException] | None,
|
|
98
|
+
exc_value: BaseException | None,
|
|
99
|
+
traceback: TracebackType | None,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.close()
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _handle_response(
|
|
105
|
+
response: httpx.Response,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""Validate and decode an API object response."""
|
|
108
|
+
if response.is_error:
|
|
109
|
+
raise SynthGraphHTTPClient._create_http_error(response)
|
|
110
|
+
|
|
111
|
+
if not response.content:
|
|
112
|
+
return {}
|
|
113
|
+
|
|
114
|
+
data = response.json()
|
|
115
|
+
|
|
116
|
+
if not isinstance(data, dict):
|
|
117
|
+
raise SynthGraphHTTPError(
|
|
118
|
+
status_code=response.status_code,
|
|
119
|
+
message="API response must be a JSON object",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return data
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _handle_list_response(
|
|
126
|
+
response: httpx.Response,
|
|
127
|
+
) -> list[dict[str, Any]]:
|
|
128
|
+
"""Validate and decode an API list response."""
|
|
129
|
+
if response.is_error:
|
|
130
|
+
raise SynthGraphHTTPClient._create_http_error(response)
|
|
131
|
+
|
|
132
|
+
if not response.content:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
data = response.json()
|
|
136
|
+
|
|
137
|
+
if not isinstance(data, list):
|
|
138
|
+
raise SynthGraphHTTPError(
|
|
139
|
+
status_code=response.status_code,
|
|
140
|
+
message="API response must be a JSON array",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not all(isinstance(item, dict) for item in data):
|
|
144
|
+
raise SynthGraphHTTPError(
|
|
145
|
+
status_code=response.status_code,
|
|
146
|
+
message="API response array must contain JSON objects",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return data
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _create_http_error(
|
|
153
|
+
response: httpx.Response,
|
|
154
|
+
) -> SynthGraphHTTPError:
|
|
155
|
+
"""Create a consistent API error from an HTTP response."""
|
|
156
|
+
try:
|
|
157
|
+
data = response.json()
|
|
158
|
+
|
|
159
|
+
if isinstance(data, dict):
|
|
160
|
+
message = str(
|
|
161
|
+
data.get(
|
|
162
|
+
"detail",
|
|
163
|
+
data.get("message", response.text),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
message = response.text
|
|
168
|
+
except ValueError:
|
|
169
|
+
message = response.text
|
|
170
|
+
|
|
171
|
+
return SynthGraphHTTPError(
|
|
172
|
+
status_code=response.status_code,
|
|
173
|
+
message=message,
|
|
174
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .experiment import Experiment
|
|
2
|
+
from .generation import GenerationRun, GenerationStatus, Generator, Reproducibility
|
|
3
|
+
from .project import Project
|
|
4
|
+
from .reference import AssetReference, DataReference, DatasetReference
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AssetReference",
|
|
8
|
+
"DataReference",
|
|
9
|
+
"DatasetReference",
|
|
10
|
+
"Experiment",
|
|
11
|
+
"GenerationRun",
|
|
12
|
+
"GenerationStatus",
|
|
13
|
+
"Generator",
|
|
14
|
+
"Project",
|
|
15
|
+
"Reproducibility",
|
|
16
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Experiment(BaseModel):
|
|
7
|
+
"""A research experiment belonging to a SynthGraph project."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(frozen=True)
|
|
10
|
+
|
|
11
|
+
id: str
|
|
12
|
+
project_id: str
|
|
13
|
+
name: str = Field(min_length=1)
|
|
14
|
+
description: str | None = None
|
|
15
|
+
created_at: datetime
|
|
16
|
+
updated_at: datetime | None = None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from .reference import DataReference
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GenerationStatus(str, Enum):
|
|
11
|
+
"""Lifecycle state of a generation run."""
|
|
12
|
+
|
|
13
|
+
PENDING = "pending"
|
|
14
|
+
RUNNING = "running"
|
|
15
|
+
COMPLETED = "completed"
|
|
16
|
+
FAILED = "failed"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Generator(BaseModel):
|
|
20
|
+
"""Information about the tool that produced a generation."""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(frozen=True)
|
|
23
|
+
|
|
24
|
+
name: str = Field(min_length=1)
|
|
25
|
+
version: str | None = None
|
|
26
|
+
type: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Reproducibility(BaseModel):
|
|
30
|
+
"""Information required to reproduce a generation."""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(frozen=True)
|
|
33
|
+
|
|
34
|
+
seed: int | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
exclude_if=lambda value: value is None,
|
|
37
|
+
)
|
|
38
|
+
code_version: str | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
exclude_if=lambda value: value is None,
|
|
41
|
+
)
|
|
42
|
+
environment: dict[str, Any] = Field(
|
|
43
|
+
default_factory=dict,
|
|
44
|
+
exclude_if=lambda value: not value,
|
|
45
|
+
)
|
|
46
|
+
configuration_hash: str | None = Field(
|
|
47
|
+
default=None,
|
|
48
|
+
exclude_if=lambda value: value is None,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GenerationRun(BaseModel):
|
|
53
|
+
"""A single synthetic data generation execution."""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(frozen=True)
|
|
56
|
+
|
|
57
|
+
id: str = Field(min_length=1)
|
|
58
|
+
experiment_id: str = Field(min_length=1)
|
|
59
|
+
name: str = Field(min_length=1)
|
|
60
|
+
description: str | None = None
|
|
61
|
+
|
|
62
|
+
generator: Generator
|
|
63
|
+
parameters: dict[str, Any]
|
|
64
|
+
|
|
65
|
+
reproducibility: Reproducibility
|
|
66
|
+
|
|
67
|
+
inputs: list[DataReference] = Field(default_factory=list)
|
|
68
|
+
outputs: list[DataReference] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
status: GenerationStatus
|
|
71
|
+
|
|
72
|
+
started_at: datetime | None = None
|
|
73
|
+
completed_at: datetime | None = None
|
|
74
|
+
created_at: datetime
|
|
75
|
+
|
|
76
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Project(BaseModel):
|
|
7
|
+
"""A top-level SynthGraph research project."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(frozen=True)
|
|
10
|
+
|
|
11
|
+
id: str
|
|
12
|
+
name: str = Field(min_length=1)
|
|
13
|
+
description: str | None = None
|
|
14
|
+
created_at: datetime
|
|
15
|
+
updated_at: datetime | None = None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DataReference(BaseModel):
|
|
7
|
+
"""Reference to data managed outside or inside SynthGraph."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(frozen=True)
|
|
10
|
+
|
|
11
|
+
id: str = Field(min_length=1)
|
|
12
|
+
uri: str = Field(min_length=1)
|
|
13
|
+
name: str = Field(min_length=1)
|
|
14
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AssetReference(DataReference):
|
|
18
|
+
"""Reference to a source or generated asset."""
|
|
19
|
+
|
|
20
|
+
type: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DatasetReference(DataReference):
|
|
24
|
+
"""Reference to a source or generated dataset."""
|
|
25
|
+
|
|
26
|
+
format: str | None = None
|
|
27
|
+
size: int | None = Field(default=None, ge=0)
|
synthgraph/projects.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .http import SynthGraphHTTPClient
|
|
4
|
+
from .models import Project
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProjectsAPI:
|
|
8
|
+
"""API operations for SynthGraph projects."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, http: SynthGraphHTTPClient) -> None:
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
def create(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
name: str,
|
|
17
|
+
description: str | None = None,
|
|
18
|
+
) -> Project:
|
|
19
|
+
"""Create a new SynthGraph project."""
|
|
20
|
+
payload: dict[str, Any] = {
|
|
21
|
+
"name": name,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if description is not None:
|
|
25
|
+
payload["description"] = description
|
|
26
|
+
|
|
27
|
+
data = self._http.post(
|
|
28
|
+
"/projects",
|
|
29
|
+
json=payload,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return Project.model_validate(data)
|
|
33
|
+
|
|
34
|
+
def get(self, project_id: str) -> Project:
|
|
35
|
+
"""Retrieve a SynthGraph project by ID."""
|
|
36
|
+
data = self._http.get(
|
|
37
|
+
f"/projects/{project_id}",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return Project.model_validate(data)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def model_to_dict(model: BaseModel) -> dict[str, Any]:
|
|
8
|
+
"""Convert a SynthGraph model into JSON-compatible Python data."""
|
|
9
|
+
return model.model_dump(mode="json")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def model_to_json(model: BaseModel) -> str:
|
|
13
|
+
"""Convert a SynthGraph model into a JSON string."""
|
|
14
|
+
return json.dumps(model_to_dict(model))
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: synthgraph-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for SynthGraph experiment provenance and lineage tracking.
|
|
5
|
+
Author: SynthGraph
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
9
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy<2.0,>=1.11; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-cov<6.0,>=5.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff<1.0,>=0.6; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# SynthGraph Python SDK
|
|
18
|
+
|
|
19
|
+
The official Python SDK for SynthGraph.
|
|
20
|
+
|
|
21
|
+
SynthGraph allows researchers to capture experiment provenance and lineage from
|
|
22
|
+
their existing research environments without moving their workflows into the
|
|
23
|
+
SynthGraph platform.
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
Early development.
|
|
28
|
+
|
|
29
|
+
The SDK is currently under active development and the public API is not yet
|
|
30
|
+
stable.
|
|
31
|
+
|
|
32
|
+
## Planned v1.0 capabilities
|
|
33
|
+
|
|
34
|
+
- Project and experiment management
|
|
35
|
+
- Synthetic-data generation provenance
|
|
36
|
+
- Dataset references
|
|
37
|
+
- Training-run provenance
|
|
38
|
+
- Evaluation metrics
|
|
39
|
+
- Asset references
|
|
40
|
+
- Code and environment metadata
|
|
41
|
+
- Experiment lineage
|
|
42
|
+
- Cloud and self-hosted SynthGraph deployments
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Install the SDK from the project directory:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e ".[dev]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
A typical SynthGraph workflow is:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Project
|
|
64
|
+
└── Experiment
|
|
65
|
+
└── Generation
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Create a project
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from synthgraph import SynthGraphClient
|
|
72
|
+
|
|
73
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
74
|
+
project = client.projects.create(
|
|
75
|
+
name="Synthetic Research",
|
|
76
|
+
description="Synthetic-data research project",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(project.id)
|
|
80
|
+
print(project.name)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Create an experiment
|
|
84
|
+
|
|
85
|
+
Experiments belong to projects:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from synthgraph import SynthGraphClient
|
|
89
|
+
|
|
90
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
91
|
+
experiment = client.experiments.create(
|
|
92
|
+
project_id="project_123",
|
|
93
|
+
name="Rainy Scene Study",
|
|
94
|
+
description="Study of synthetic rainy-scene generation",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
print(experiment.id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Record a synthetic-data generation
|
|
101
|
+
|
|
102
|
+
Use `Generator` to describe the software or tool that produced the result,
|
|
103
|
+
and `Reproducibility` to capture information needed to reproduce the run.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from synthgraph import (
|
|
107
|
+
Generator,
|
|
108
|
+
Reproducibility,
|
|
109
|
+
SynthGraphClient,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
113
|
+
generation = client.generations.create(
|
|
114
|
+
experiment_id="experiment_123",
|
|
115
|
+
name="Rainy Scene Generation",
|
|
116
|
+
generator=Generator(
|
|
117
|
+
name="blender",
|
|
118
|
+
version="4.2.0",
|
|
119
|
+
type="3d_renderer",
|
|
120
|
+
),
|
|
121
|
+
parameters={
|
|
122
|
+
"samples": 512,
|
|
123
|
+
"weather": "rain",
|
|
124
|
+
},
|
|
125
|
+
reproducibility=Reproducibility(
|
|
126
|
+
seed=42,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
print(generation.id)
|
|
131
|
+
print(generation.status)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Record input and output references
|
|
135
|
+
|
|
136
|
+
Generations can reference assets and datasets without moving the underlying
|
|
137
|
+
data into SynthGraph.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from synthgraph import (
|
|
141
|
+
AssetReference,
|
|
142
|
+
DatasetReference,
|
|
143
|
+
Generator,
|
|
144
|
+
Reproducibility,
|
|
145
|
+
SynthGraphClient,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
input_asset = AssetReference(
|
|
149
|
+
id="asset_123",
|
|
150
|
+
uri="file:///data/model.blend",
|
|
151
|
+
name="model.blend",
|
|
152
|
+
type="3d_model",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
input_dataset = DatasetReference(
|
|
156
|
+
id="dataset_123",
|
|
157
|
+
uri="s3://bucket/input-scenes",
|
|
158
|
+
name="input-scenes",
|
|
159
|
+
format="image",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
output_dataset = DatasetReference(
|
|
163
|
+
id="dataset_456",
|
|
164
|
+
uri="s3://bucket/output-scenes",
|
|
165
|
+
name="output-scenes",
|
|
166
|
+
format="image",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
170
|
+
generation = client.generations.create(
|
|
171
|
+
experiment_id="experiment_123",
|
|
172
|
+
name="Referenced Generation",
|
|
173
|
+
generator=Generator(name="blender", version="4.2.0"),
|
|
174
|
+
parameters={
|
|
175
|
+
"samples": 512,
|
|
176
|
+
},
|
|
177
|
+
reproducibility=Reproducibility(seed=42),
|
|
178
|
+
inputs=[input_asset, input_dataset],
|
|
179
|
+
outputs=[output_dataset],
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
References contain metadata about the data location and identity. The SDK
|
|
184
|
+
does not upload the referenced files or datasets.
|
|
185
|
+
|
|
186
|
+
## Reading existing resources
|
|
187
|
+
|
|
188
|
+
Resources can be retrieved by ID.
|
|
189
|
+
|
|
190
|
+
### Get a project
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
194
|
+
project = client.projects.get("project_123")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Get an experiment
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
201
|
+
experiment = client.experiments.get("experiment_123")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Get a generation
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
208
|
+
generation = client.generations.get("generation_123")
|
|
209
|
+
print(generation.status)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Listing resources
|
|
213
|
+
|
|
214
|
+
### List experiments
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
218
|
+
experiments = client.experiments.list(
|
|
219
|
+
project_id="project_123",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
for experiment in experiments:
|
|
223
|
+
print(experiment.id, experiment.name)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### List generations
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
with SynthGraphClient(api_key="your-api-key") as client:
|
|
230
|
+
generations = client.generations.list(
|
|
231
|
+
experiment_id="experiment_123",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
for generation in generations:
|
|
235
|
+
print(generation.id, generation.status)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Configuration
|
|
239
|
+
|
|
240
|
+
The client can be configured directly:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from synthgraph import SynthGraphClient
|
|
244
|
+
|
|
245
|
+
client = SynthGraphClient(
|
|
246
|
+
api_key="your-api-key",
|
|
247
|
+
api_url="https://api.example.com",
|
|
248
|
+
timeout=30.0,
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Or through `SynthGraphConfig`:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from synthgraph import SynthGraphClient, SynthGraphConfig
|
|
256
|
+
|
|
257
|
+
config = SynthGraphConfig(
|
|
258
|
+
api_key="your-api-key",
|
|
259
|
+
api_url="https://api.example.com",
|
|
260
|
+
timeout=30.0,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
with SynthGraphClient(config=config) as client:
|
|
264
|
+
project = client.projects.get("project_123")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Do not combine `config` with `api_key`, `api_url`, or `timeout` on the same
|
|
268
|
+
`SynthGraphClient` instance.
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
From this directory:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
pip install -e ".[dev]"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Run tests:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
pytest
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Run linting:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
ruff check .
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Run type checking:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
mypy src
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Current SDK surface
|
|
297
|
+
|
|
298
|
+
The current public client exposes:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
with SynthGraphClient(...) as client:
|
|
302
|
+
client.projects
|
|
303
|
+
client.experiments
|
|
304
|
+
client.generations
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The SDK currently supports:
|
|
308
|
+
|
|
309
|
+
- Project creation, retrieval, and listing
|
|
310
|
+
- Experiment creation, retrieval, and listing
|
|
311
|
+
- Generation creation, retrieval, and listing
|
|
312
|
+
- Asset references
|
|
313
|
+
- Dataset references
|
|
314
|
+
- Generator metadata
|
|
315
|
+
- Reproducibility metadata
|
|
316
|
+
- HTTP error handling
|
|
317
|
+
- Custom API URLs
|
|
318
|
+
- Custom HTTP transports for testing
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
Apache-2.0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
synthgraph/__init__.py,sha256=IrMm4zmbCNo-d0D4fTJqhyevDs7Pf9jeTv0n0xUUuOY,736
|
|
2
|
+
synthgraph/client.py,sha256=DsMTlD0OTSu1W5jLryQM1Ex1tpNPKL1nYgRxzUJMVuE,2023
|
|
3
|
+
synthgraph/config.py,sha256=dIKy6c6x8LfNDljDjz7PafYqSXmlAqdlBOm-HNY9HXY,639
|
|
4
|
+
synthgraph/experiments.py,sha256=QhPXf2qJKdclj5ab4F8V-89JUhiNlqz4-pCPHkTNk2w,1383
|
|
5
|
+
synthgraph/generations.py,sha256=2EzBPdmaDSezz0dh28FL3MASkbVTphATOxI2h90qyog,3762
|
|
6
|
+
synthgraph/http.py,sha256=kLnLZNm4FCjsS98xGeZIq9XeTAH4iAADf4v20RfKH4I,4991
|
|
7
|
+
synthgraph/projects.py,sha256=jXRs207l_ZM70sYvTUIhAWZ9DfD9irjaDCHfY0LqpW0,974
|
|
8
|
+
synthgraph/models/__init__.py,sha256=4kgit23Hx2ro0QUPSdhEn0cc2A3Las_EqRuyL_AM5v4,433
|
|
9
|
+
synthgraph/models/experiment.py,sha256=02kJR3f_2M2wOQ1WbRCE3naB93FNLv8ls6Ld4cV1P-s,405
|
|
10
|
+
synthgraph/models/generation.py,sha256=xZEEfAZiNqxliKmOuf5a-TGpmodl_GQAEuOYTWMc6Aw,1958
|
|
11
|
+
synthgraph/models/project.py,sha256=A9vQb_hgIUzaoc--gXTzOYJNkkABqGM6Br5K0G5bOjA,365
|
|
12
|
+
synthgraph/models/reference.py,sha256=V57a0wd_Q68Ix28BvimU9yYhiUT6OrFl0efFMypuaxU,703
|
|
13
|
+
synthgraph/serialization/__init__.py,sha256=So31eYUfXRWX9Ys3TxQQ_f7plNrpQZG6zrmHkhUTpHo,94
|
|
14
|
+
synthgraph/serialization/json.py,sha256=CnEi4BSKYqJzTObUvxcBHu0sxBAhFxlxdwxy1ALO-p8,395
|
|
15
|
+
synthgraph_sdk-0.1.0.dist-info/METADATA,sha256=XxiZbJhTKdQqyfPWwduuVxfE_ECWpdfmWiLTqD8R8a4,6687
|
|
16
|
+
synthgraph_sdk-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
17
|
+
synthgraph_sdk-0.1.0.dist-info/RECORD,,
|