Skip to content

Commit 3a77919

Browse files
committed
Support Python 3.8 and compatible editable packaging
1 parent 00bdbec commit 3a77919

6 files changed

Lines changed: 36 additions & 32 deletions

File tree

‎.github/workflows/tests.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python: ['3.10', '3.12', '3.13']
10+
python: ['3.8', '3.10', '3.12', '3.13']
1111
steps:
1212
- uses: actions/checkout@v4
1313
- uses: actions/setup-python@v5

‎README.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ BYO workers belong in the separate worker package, not this client.
66

77
## Development install
88

9-
Requires Python 3.10+ and a modern pip (21.3+ for editable installs).
9+
Requires Python 3.8+ and a modern pip (21.3+ for editable installs).
1010
Use a supported interpreter explicitly; the system `python3` may be older.
1111

1212
```bash
13-
python3.12 -m venv .venv
13+
python3 -m venv .venv
1414
source .venv/bin/activate
1515
python -m pip install --upgrade pip
1616
python -m pip install -e '.[dev]'
@@ -20,9 +20,10 @@ python -m twine check dist/*
2020
```
2121

2222
If pip reports that editable mode requires `setup.py`, check `python --version`
23-
and `python -m pip --version`. Python 3.8 is unsupported. Create a new environment
24-
with Python 3.10+ and upgrade pip there; upgrading pip alone cannot change its
25-
Python interpreter. A `setup.py` compatibility shim is not needed.
23+
and `python -m pip --version`. Upgrade pip inside the active environment:
24+
`python -m pip install --upgrade pip`. Python 3.8 is supported; pip will select
25+
the latest pip and dependencies compatible with that interpreter. A `setup.py`
26+
compatibility shim is not needed.
2627

2728
With [uv](https://docs.astral.sh/uv/), a supported Python can also be provisioned
2829
without changing the system Python. Preserve an existing environment by using a

‎pyproject.toml‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
[build-system]
2-
requires = ["setuptools>=77", "wheel"]
2+
requires = ["setuptools>=61", "wheel"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "subfork"
77
version = "0.1.0a1"
88
description = "Python API client for Subfork graph authoring and execution"
99
readme = "README.md"
10-
license = "BSD-3-Clause"
11-
license-files = ["LICENSE"]
12-
requires-python = ">=3.10"
10+
license = {file = "LICENSE"}
11+
requires-python = ">=3.8"
1312
dependencies = ["httpx>=0.28,<1"]
1413
classifiers = [
1514
"Development Status :: 3 - Alpha",
@@ -21,6 +20,9 @@ classifiers = [
2120
[project.optional-dependencies]
2221
dev = ["pytest>=8,<10", "build>=1,<2", "twine>=6,<7"]
2322

23+
[tool.setuptools]
24+
license-files = ["LICENSE"]
25+
2426
[tool.setuptools.packages.find]
2527
where = ["src"]
2628

‎src/subfork/client.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import math
55
import os
6-
from typing import Any
6+
from typing import Any, Optional
77

88
import httpx
99

@@ -12,8 +12,8 @@
1212

1313

1414
class Subfork:
15-
def __init__(self, api_key: str | None = None, *, base_url: str = 'https://subfork.com',
16-
timeout: float = 30, transport: httpx.BaseTransport | None = None):
15+
def __init__(self, api_key: Optional[str] = None, *, base_url: str = 'https://subfork.com',
16+
timeout: float = 30, transport: Optional[httpx.BaseTransport] = None):
1717
key = os.environ.get('SUBFORK_API_KEY') if api_key is None else api_key
1818
if not key or any(char.isspace() for char in key):
1919
raise ValueError('Provide an API key or set SUBFORK_API_KEY; keys cannot contain whitespace.')

‎src/subfork/errors.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Public client errors; credentials and raw responses are never attached."""
2+
from typing import Optional
23

34

45
class SubforkError(Exception):
@@ -10,7 +11,7 @@ class TransportError(SubforkError):
1011

1112

1213
class APIError(SubforkError):
13-
def __init__(self, message: str, *, status_code: int, retry_after: str | None = None):
14+
def __init__(self, message: str, *, status_code: int, retry_after: Optional[str] = None):
1415
super().__init__(message)
1516
self.status_code = status_code
1617
self.retry_after = retry_after

‎src/subfork/resources.py‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import math
55
import time
6-
from typing import TYPE_CHECKING, Any
6+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
77
from urllib.parse import quote
88

99
from .errors import ExecutionTimeout
@@ -24,63 +24,63 @@ def __init__(self, client: Subfork):
2424

2525

2626
class Nodes(Resource):
27-
def list(self, *, include_all_versions: bool = False) -> list[dict[str, Any]]:
27+
def list(self, *, include_all_versions: bool = False) -> List[Dict[str, Any]]:
2828
return self._client._request('GET', '/nodes', params={'include_all_versions': include_all_versions})
2929

30-
def get(self, node_id: str) -> dict[str, Any]:
30+
def get(self, node_id: str) -> Dict[str, Any]:
3131
return self._client._request('GET', '/nodes/' + segment(node_id))
3232

3333

3434
class Graphs(Resource):
35-
def list(self) -> list[dict[str, Any]]:
35+
def list(self) -> List[Dict[str, Any]]:
3636
return self._client._request('GET', '/graphs')
3737

38-
def get(self, graph_id: str) -> dict[str, Any]:
38+
def get(self, graph_id: str) -> Dict[str, Any]:
3939
return self._client._request('GET', '/graphs/' + segment(graph_id))
4040

41-
def validate(self, definition: dict[str, Any], *, name: str | None = None) -> dict[str, Any]:
41+
def validate(self, definition: Dict[str, Any], *, name: Optional[str] = None) -> Dict[str, Any]:
4242
return self._client._request('POST', '/graphs/validate', json={
4343
'name': name if name is not None else definition.get('name', 'Untitled Graph'), 'definition': definition})
4444

45-
def create(self, *, name: str, definition: dict[str, Any], description: str = '', tags: list[str] | None = None) -> dict[str, Any]:
45+
def create(self, *, name: str, definition: Dict[str, Any], description: str = '', tags: Optional[List[str]] = None) -> Dict[str, Any]:
4646
return self._client._request('POST', '/graphs', json={
4747
'name': name, 'description': description, 'definition': definition, 'tags': tags or []})
4848

49-
def update(self, graph_id: str, *, name: str, definition: dict[str, Any], description: str = '') -> dict[str, Any]:
49+
def update(self, graph_id: str, *, name: str, definition: Dict[str, Any], description: str = '') -> Dict[str, Any]:
5050
return self._client._request('PUT', '/graphs/' + segment(graph_id), json={
5151
'name': name, 'description': description, 'definition': definition})
5252

53-
def published(self) -> list[dict[str, Any]]:
53+
def published(self) -> List[Dict[str, Any]]:
5454
return self._client._request('GET', '/graphs/published')
5555

56-
def versions(self, graph_id: str) -> list[dict[str, Any]]:
56+
def versions(self, graph_id: str) -> List[Dict[str, Any]]:
5757
return self._client._request('GET', '/graphs/' + segment(graph_id) + '/versions')
5858

59-
def published_version(self, graph_id: str, version: str) -> dict[str, Any]:
59+
def published_version(self, graph_id: str, version: str) -> Dict[str, Any]:
6060
return self._client._request('GET', '/graphs/published/' + segment(graph_id) + '/versions/' + segment(version))
6161

62-
def interface(self, graph_id: str) -> dict[str, Any]:
62+
def interface(self, graph_id: str) -> Dict[str, Any]:
6363
return self._client._request('GET', '/graphs/' + segment(graph_id) + '/interface-preview')
6464

65-
def publish(self, graph_id: str, *, version: str, interface: dict[str, Any] | None = None, comment: str = '') -> dict[str, Any]:
66-
payload: dict[str, Any] = {'version': version, 'comment': comment}
65+
def publish(self, graph_id: str, *, version: str, interface: Optional[Dict[str, Any]] = None, comment: str = '') -> Dict[str, Any]:
66+
payload: Dict[str, Any] = {'version': version, 'comment': comment}
6767
if interface is not None:
6868
payload['interface'] = interface
6969
return self._client._request('POST', '/graphs/' + segment(graph_id) + '/versions', json=payload)
7070

71-
def execute(self, graph_id: str, *, version: str = 'draft', inputs: dict[str, Any] | None = None) -> dict[str, Any]:
71+
def execute(self, graph_id: str, *, version: str = 'draft', inputs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
7272
return self._client._request('POST', '/graphs/' + segment(graph_id) + '/execute',
7373
params={'version': version}, json={'inputs': inputs or {}})
7474

7575

7676
class Executions(Resource):
77-
def get(self, execution_id: str) -> dict[str, Any]:
77+
def get(self, execution_id: str) -> Dict[str, Any]:
7878
return self._client._request('GET', '/executions/' + segment(execution_id))
7979

80-
def cancel(self, execution_id: str) -> dict[str, Any]:
80+
def cancel(self, execution_id: str) -> Dict[str, Any]:
8181
return self._client._request('POST', '/executions/' + segment(execution_id) + '/cancel')
8282

83-
def wait(self, execution_id: str, *, timeout: float = 120, poll_interval: float = 2) -> dict[str, Any]:
83+
def wait(self, execution_id: str, *, timeout: float = 120, poll_interval: float = 2) -> Dict[str, Any]:
8484
if not math.isfinite(timeout) or timeout <= 0 or not math.isfinite(poll_interval) or poll_interval <= 0:
8585
raise ValueError('timeout and poll_interval must be positive finite numbers.')
8686
deadline = time.monotonic() + timeout

0 commit comments

Comments
 (0)