Skip to content

Commit e990c5f

Browse files
committed
Add support for Python 3.9
1 parent e21cc9a commit e990c5f

File tree

7 files changed

+116
-122
lines changed

7 files changed

+116
-122
lines changed

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
fail-fast: false
1111
matrix:
12-
python-version: ["3.10", "3.11"]
12+
python-version: ["3.9", "3.10", "3.11"]
1313
steps:
1414
- uses: actions/checkout@v4
1515
- name: Set up Python ${{ matrix.python-version }}

metafold/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from datetime import datetime, timezone
2-
from typing import Any
2+
from typing import Any, Union
33

44

5-
def asdatetime(s: str | datetime) -> datetime:
5+
def asdatetime(s: Union[str, datetime]) -> datetime:
66
"""Parse Metafold API datetime.
77
88
Note datetime strings returned by the Metafold API are RFC 1123 formatted,

metafold/assets.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from metafold.client import Client
55
from os import PathLike
66
from requests import Response
7-
from typing import IO, Optional
7+
from typing import IO, Optional, Union
88
import requests
99

1010

@@ -65,7 +65,7 @@ def get(self, id: str) -> Asset:
6565
r: Response = self._client.get(url)
6666
return Asset(**r.json())
6767

68-
def download_file(self, id: str, path: str | PathLike):
68+
def download_file(self, id: str, path: Union[str, PathLike]):
6969
"""Download an asset.
7070
7171
Args:
@@ -79,7 +79,7 @@ def download_file(self, id: str, path: str | PathLike):
7979
for chunk in r.iter_content(chunk_size=65536): # 64 KiB
8080
f.write(chunk)
8181

82-
def create(self, f: str | bytes | PathLike | IO[bytes]) -> Asset:
82+
def create(self, f: Union[str, bytes, PathLike, IO[bytes]]) -> Asset:
8383
"""Upload an asset.
8484
8585
Args:
@@ -96,7 +96,7 @@ def create(self, f: str | bytes | PathLike | IO[bytes]) -> Asset:
9696
fp.close()
9797
return Asset(**r.json())
9898

99-
def update(self, id: str, f: str | bytes | PathLike | IO[bytes]) -> Asset:
99+
def update(self, id: str, f: Union[str, bytes, PathLike, IO[bytes]]) -> Asset:
100100
"""Update an asset.
101101
102102
Args:
@@ -124,7 +124,7 @@ def delete(self, id: str) -> None:
124124
self._client.delete(url)
125125

126126

127-
def _open_file(f: str | bytes | PathLike | IO[bytes]) -> IO[bytes]:
128-
if isinstance(f, str | bytes | PathLike):
127+
def _open_file(f: Union[str, bytes, PathLike, IO[bytes]]) -> IO[bytes]:
128+
if isinstance(f, (str, bytes, PathLike)):
129129
return open(f, "rb")
130130
return f

metafold/jobs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
from metafold.client import Client
66
from metafold.exceptions import PollTimeout
77
from requests import Response
8-
from typing import Any, Optional
8+
from typing import Any, Optional, Union
99
import time
1010

1111

12-
def _assets(v: list[dict[str, Any] | Asset]) -> list[Asset]:
12+
def _assets(v: list[Union[dict[str, Any], Asset]]) -> list[Asset]:
1313
return [a if isinstance(a, Asset) else Asset(**a) for a in v]
1414

1515

@@ -76,7 +76,7 @@ def get(self, id: str) -> Job:
7676
def run(
7777
self, type: str, params: dict[str, Any],
7878
name: Optional[str] = None,
79-
timeout: int | float = 120,
79+
timeout: Union[int, float] = 120,
8080
) -> Job:
8181
"""Dispatch a new job and wait for a result.
8282
@@ -111,7 +111,7 @@ def update(self, id: str, name: Optional[str] = None) -> Job:
111111
r: Response = self._client.patch(url, data=payload)
112112
return Job(**r.json())
113113

114-
def _poll(self, url: str, timeout: int | float) -> Response:
114+
def _poll(self, url: str, timeout: Union[int, float]) -> Response:
115115
t0 = time.monotonic()
116116
r = self._client.get(url)
117117
while r.status_code == 202:

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "metafold"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
authors = [
99
{name = "Metafold 3D", email = "[email protected]"},
1010
]
@@ -15,14 +15,15 @@ dependencies = [
1515
readme = "README.md"
1616
license = {file = "LICENSE"}
1717
description = "Metafold SDK for Python"
18-
requires-python = ">=3.10"
18+
requires-python = ">=3.9"
1919
keywords = ["metafold", "api", "sdk", "implicit geometry"]
2020
classifiers = [
2121
"Development Status :: 3 - Alpha",
2222
"Intended Audience :: Developers",
2323
"Intended Audience :: Science/Research",
2424
"License :: OSI Approved :: MIT License",
2525
"Operating System :: OS Independent",
26+
"Programming Language :: Python :: 3.9",
2627
"Programming Language :: Python :: 3.10",
2728
"Programming Language :: Python :: 3.11",
2829
"Topic :: Scientific/Engineering",

tests/test_assets.py

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -57,62 +57,58 @@ class MockRequestHandler(BaseHTTPRequestHandler):
5757
def do_GET(self):
5858
u = urlparse(self.path)
5959
params = parse_qs(u.query)
60-
match u.path:
61-
case "/projects/1/assets":
62-
self.send_response(HTTPStatus.OK)
63-
self.send_header("Content-Type", "application/json")
64-
self.end_headers()
65-
payload = asset_list
66-
if params.get("sort") == ["id:1"]:
67-
payload = sorted(asset_list, key=lambda p: p["id"])
68-
elif params.get("q") == ["filename:f763df409e79eb1c.bin"]:
69-
payload = [p for p in asset_list if p["filename"] == "f763df409e79eb1c.bin"]
70-
self.wfile.write(json.dumps(payload).encode())
71-
case "/projects/1/assets/1":
72-
self.send_response(HTTPStatus.OK)
73-
self.send_header("Content-Type", "application/json")
74-
self.end_headers()
75-
payload = deepcopy(asset_list[-1])
76-
if params.get("download") == ["true"]:
77-
payload["link"] = "http://localhost:8000/download?filename=f763df409e79eb1c.bin"
78-
self.wfile.write(json.dumps(payload).encode())
79-
case "/download":
80-
self.send_response(HTTPStatus.OK)
81-
self.end_headers()
82-
with open(test_file, "rb") as f:
83-
self.wfile.write(f.read())
84-
case _:
85-
self.send_error(HTTPStatus.NOT_FOUND)
60+
if u.path == "/projects/1/assets":
61+
self.send_response(HTTPStatus.OK)
62+
self.send_header("Content-Type", "application/json")
63+
self.end_headers()
64+
payload = asset_list
65+
if params.get("sort") == ["id:1"]:
66+
payload = sorted(asset_list, key=lambda p: p["id"])
67+
elif params.get("q") == ["filename:f763df409e79eb1c.bin"]:
68+
payload = [p for p in asset_list if p["filename"] == "f763df409e79eb1c.bin"]
69+
self.wfile.write(json.dumps(payload).encode())
70+
elif u.path == "/projects/1/assets/1":
71+
self.send_response(HTTPStatus.OK)
72+
self.send_header("Content-Type", "application/json")
73+
self.end_headers()
74+
payload = deepcopy(asset_list[-1])
75+
if params.get("download") == ["true"]:
76+
payload["link"] = "http://localhost:8000/download?filename=f763df409e79eb1c.bin"
77+
self.wfile.write(json.dumps(payload).encode())
78+
elif u.path == "/download":
79+
self.send_response(HTTPStatus.OK)
80+
self.end_headers()
81+
with open(test_file, "rb") as f:
82+
self.wfile.write(f.read())
83+
else:
84+
self.send_error(HTTPStatus.NOT_FOUND)
8685

8786
def do_POST(self):
88-
match self.path:
89-
case "/projects/1/assets":
90-
self.send_response(HTTPStatus.CREATED)
91-
self.send_header("Content-Type", "application/json")
92-
self.end_headers()
93-
self._assert_file()
94-
self.wfile.write(json.dumps(new_asset).encode())
95-
case _:
96-
self.send_error(HTTPStatus.NOT_FOUND)
87+
if self.path == "/projects/1/assets":
88+
self.send_response(HTTPStatus.CREATED)
89+
self.send_header("Content-Type", "application/json")
90+
self.end_headers()
91+
self._assert_file()
92+
self.wfile.write(json.dumps(new_asset).encode())
93+
else:
94+
self.send_error(HTTPStatus.NOT_FOUND)
9795

9896
def do_PATCH(self):
99-
match self.path:
100-
case "/projects/1/assets/1":
101-
self.send_response(HTTPStatus.OK)
102-
self.send_header("Content-Type", "application/json")
103-
self.end_headers()
104-
self._assert_file()
105-
self.wfile.write(json.dumps(new_asset).encode())
106-
case _:
107-
self.send_error(HTTPStatus.NOT_FOUND)
97+
if self.path == "/projects/1/assets/1":
98+
self.send_response(HTTPStatus.OK)
99+
self.send_header("Content-Type", "application/json")
100+
self.end_headers()
101+
self._assert_file()
102+
self.wfile.write(json.dumps(new_asset).encode())
103+
else:
104+
self.send_error(HTTPStatus.NOT_FOUND)
108105

109106
def do_DELETE(self):
110-
match self.path:
111-
case "/projects/1/assets/1":
112-
self.send_response(HTTPStatus.OK)
113-
self.end_headers()
114-
case _:
115-
self.send_error(HTTPStatus.NOT_FOUND)
107+
if self.path == "/projects/1/assets/1":
108+
self.send_response(HTTPStatus.OK)
109+
self.end_headers()
110+
else:
111+
self.send_error(HTTPStatus.NOT_FOUND)
116112

117113
def _assert_file(self):
118114
length = self.headers.get("Content-Length")

tests/test_jobs.py

Lines changed: 54 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -89,69 +89,66 @@ class MockRequestHandler(BaseHTTPRequestHandler):
8989
def do_GET(self):
9090
u = urlparse(self.path)
9191
params = parse_qs(u.query)
92-
match u.path:
93-
case "/projects/1/jobs":
94-
self.send_response(HTTPStatus.OK)
95-
self.send_header("Content-Type", "application/json")
96-
self.end_headers()
97-
payload = job_list
98-
if params.get("sort") == ["id:1"]:
99-
payload = sorted(job_list, key=lambda p: p["id"])
100-
elif params.get("q") == ["name:foo"]:
101-
payload = [p for p in job_list if p["name"] == "foo"]
102-
self.wfile.write(json.dumps(payload).encode())
103-
case "/projects/1/jobs/1":
104-
self.send_response(HTTPStatus.OK)
105-
self.send_header("Content-Type", "application/json")
106-
self.end_headers()
107-
payload = job_list[-1]
108-
self.wfile.write(json.dumps(payload).encode())
109-
case "/projects/1/jobs/1/status":
110-
global poll_count
111-
poll_count += 1
112-
if poll_count < 3:
113-
self.send_response(HTTPStatus.ACCEPTED)
114-
payload = deepcopy(new_job)
115-
payload["state"] = "started"
116-
else:
117-
self.send_response(HTTPStatus.CREATED)
118-
payload = deepcopy(new_job)
119-
payload.update({
120-
"state": "success",
121-
"assets": [asset_json],
122-
})
123-
self.send_header("Content-Type", "application/json")
124-
self.end_headers()
125-
self.wfile.write(json.dumps(payload).encode())
126-
case _:
127-
self.send_error(HTTPStatus.NOT_FOUND)
128-
129-
def do_POST(self):
130-
match self.path:
131-
case "/projects/1/jobs":
92+
if u.path == "/projects/1/jobs":
93+
self.send_response(HTTPStatus.OK)
94+
self.send_header("Content-Type", "application/json")
95+
self.end_headers()
96+
payload = job_list
97+
if params.get("sort") == ["id:1"]:
98+
payload = sorted(job_list, key=lambda p: p["id"])
99+
elif params.get("q") == ["name:foo"]:
100+
payload = [p for p in job_list if p["name"] == "foo"]
101+
self.wfile.write(json.dumps(payload).encode())
102+
elif u.path == "/projects/1/jobs/1":
103+
self.send_response(HTTPStatus.OK)
104+
self.send_header("Content-Type", "application/json")
105+
self.end_headers()
106+
payload = job_list[-1]
107+
self.wfile.write(json.dumps(payload).encode())
108+
elif u.path == "/projects/1/jobs/1/status":
109+
global poll_count
110+
poll_count += 1
111+
if poll_count < 3:
132112
self.send_response(HTTPStatus.ACCEPTED)
133-
self.send_header("Content-Type", "application/json")
134-
self.end_headers()
113+
payload = deepcopy(new_job)
114+
payload["state"] = "started"
115+
else:
116+
self.send_response(HTTPStatus.CREATED)
135117
payload = deepcopy(new_job)
136118
payload.update({
137-
"state": "pending",
138-
"link": "http://localhost:8000/projects/1/jobs/1/status",
119+
"state": "success",
120+
"assets": [asset_json],
139121
})
140-
self.wfile.write(json.dumps(payload).encode())
141-
case _:
142-
self.send_error(HTTPStatus.NOT_FOUND)
122+
self.send_header("Content-Type", "application/json")
123+
self.end_headers()
124+
self.wfile.write(json.dumps(payload).encode())
125+
else:
126+
self.send_error(HTTPStatus.NOT_FOUND)
127+
128+
def do_POST(self):
129+
if self.path == "/projects/1/jobs":
130+
self.send_response(HTTPStatus.ACCEPTED)
131+
self.send_header("Content-Type", "application/json")
132+
self.end_headers()
133+
payload = deepcopy(new_job)
134+
payload.update({
135+
"state": "pending",
136+
"link": "http://localhost:8000/projects/1/jobs/1/status",
137+
})
138+
self.wfile.write(json.dumps(payload).encode())
139+
else:
140+
self.send_error(HTTPStatus.NOT_FOUND)
143141

144142
def do_PATCH(self):
145-
match self.path:
146-
case "/projects/1/jobs/1":
147-
self.send_response(HTTPStatus.OK)
148-
self.send_header("Content-Type", "application/json")
149-
self.end_headers()
150-
payload = deepcopy(job_list[-1])
151-
payload["name"] = "baz"
152-
self.wfile.write(json.dumps(payload).encode())
153-
case _:
154-
self.send_error(HTTPStatus.NOT_FOUND)
143+
if self.path == "/projects/1/jobs/1":
144+
self.send_response(HTTPStatus.OK)
145+
self.send_header("Content-Type", "application/json")
146+
self.end_headers()
147+
payload = deepcopy(job_list[-1])
148+
payload["name"] = "baz"
149+
self.wfile.write(json.dumps(payload).encode())
150+
else:
151+
self.send_error(HTTPStatus.NOT_FOUND)
155152

156153

157154
@pytest.fixture(scope="module")

0 commit comments

Comments
 (0)