Skip to content

Commit f59a782

Browse files
committed
♻️ fix 3.9 support
1 parent 492378f commit f59a782

File tree

14 files changed

+193
-169
lines changed

14 files changed

+193
-169
lines changed

.github/workflows/auto-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- uses: actions/checkout@v2
1313
- uses: eifinger/setup-rye@v1
1414
with:
15-
python-version: 3.10
15+
python-version: 3.9
1616
enable_cache: true
1717
cache_prefix: "venv-codeboxapi"
1818
- run: rye sync

.github/workflows/code-check.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
pre-commit:
77
strategy:
88
matrix:
9-
python-version: ["3.10", "3.11", "3.12"]
9+
python-version: ["3.9", "3.10", "3.11", "3.12"]
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v2
@@ -18,8 +18,6 @@ jobs:
1818
run: rye pin ${{ matrix.python-version }}
1919
- name: Sync rye
2020
run: rye sync
21-
- name: Run ruff
22-
run: rye run ruff check
2321
- name: Run tests
2422
env:
2523
CODEBOX_API_KEY: ${{ secrets.CODEBOX_API_KEY }}

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/checkout@v3
1515
- uses: actions/setup-python@v4
1616
with:
17-
python-version: 3.10
17+
python-version: 3.9
1818
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
1919
- uses: actions/cache@v3
2020
with:

.github/workflows/python-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Python
1717
uses: actions/setup-python@v3
1818
with:
19-
python-version: "3.10"
19+
python-version: "3.9"
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip

examples/plot_dataset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
# download the iris dataset
1313
iris_csv_bytes = httpx.get(
14-
"https://archive.ics.uci.edu/" "ml/machine-learning-databases/iris/iris.data"
14+
"https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"
1515
).content
1616

1717
# upload the dataset to the codebox

requirements.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# all-features: false
88
# with-sources: false
99
# generate-hashes: false
10+
# universal: false
1011

1112
-e file:.
1213
anyio==4.6.2.post1

src/codeboxapi/api.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
import asyncio
22
from contextlib import asynccontextmanager
3-
from datetime import UTC, datetime, timedelta
3+
from datetime import datetime, timedelta
44
from os import getenv, path
5-
from typing import AsyncGenerator, Literal
5+
import typing as t
66

77
from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile
88
from fastapi.responses import FileResponse, StreamingResponse
99
from pydantic import BaseModel
1010

11+
from codeboxapi.utils import async_raise_timeout
12+
1113
from .local import LocalBox
1214

1315
codebox = LocalBox()
14-
last_interaction = datetime.now(UTC)
16+
last_interaction = datetime.utcnow()
1517

1618

1719
@asynccontextmanager
18-
async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
20+
async def lifespan(_: FastAPI) -> t.AsyncGenerator[None, None]:
1921
async def timeout():
2022
if (_timeout := getenv("CODEBOX_TIMEOUT", "15")).lower() == "none":
2123
return
22-
while last_interaction + timedelta(minutes=float(_timeout)) > datetime.now(UTC):
24+
while last_interaction + timedelta(minutes=float(_timeout)) > datetime.utcnow():
2325
await asyncio.sleep(1)
2426
exit(0)
2527

@@ -28,9 +30,9 @@ async def timeout():
2830
t.cancel()
2931

3032

31-
async def get_codebox() -> AsyncGenerator[LocalBox, None]:
33+
async def get_codebox() -> t.AsyncGenerator[LocalBox, None]:
3234
global codebox, last_interaction
33-
last_interaction = datetime.now(UTC)
35+
last_interaction = datetime.utcnow()
3436
yield codebox
3537

3638

@@ -40,16 +42,16 @@ async def get_codebox() -> AsyncGenerator[LocalBox, None]:
4042

4143
class ExecBody(BaseModel):
4244
code: str
43-
kernel: Literal["ipython", "bash"] = "ipython"
44-
timeout: int | None = None
45-
cwd: str | None = None
45+
kernel: t.Literal["ipython", "bash"] = "ipython"
46+
timeout: t.Optional[int] = None
47+
cwd: t.Optional[str] = None
4648

4749

4850
@app.post("/exec")
4951
async def exec(
5052
exec: ExecBody, codebox: LocalBox = Depends(get_codebox)
5153
) -> StreamingResponse:
52-
async def event_stream() -> AsyncGenerator[str, None]:
54+
async def event_stream() -> t.AsyncGenerator[str, None]:
5355
async for chunk in codebox.astream_exec(
5456
exec.code, exec.kernel, exec.timeout, exec.cwd
5557
): # protocol is <type>content</type>
@@ -61,10 +63,10 @@ async def event_stream() -> AsyncGenerator[str, None]:
6163
@app.get("/files/download/{file_name}")
6264
async def download(
6365
file_name: str,
64-
timeout: int | None = None,
66+
timeout: t.Optional[int] = None,
6567
codebox: LocalBox = Depends(get_codebox),
6668
) -> FileResponse:
67-
async with asyncio.timeout(timeout):
69+
async with async_raise_timeout(timeout):
6870
file_path = path.join(codebox.cwd, file_name)
6971
return FileResponse(
7072
path=file_path, media_type="application/octet-stream", filename=file_name
@@ -74,13 +76,13 @@ async def download(
7476
@app.post("/files/upload")
7577
async def upload(
7678
file: UploadFile,
77-
timeout: int | None = None,
79+
timeout: t.Optional[int] = None,
7880
codebox: LocalBox = Depends(get_codebox),
7981
) -> None:
8082
if not file.filename:
8183
raise HTTPException(status_code=400, detail="A file name is required")
82-
async with asyncio.timeout(timeout):
83-
await codebox.aupload(file.filename, file.file)
84+
85+
await codebox.aupload(file.filename, file.file, timeout)
8486

8587

8688
@app.post("/code/execute")

src/codeboxapi/codebox.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@
5050
class CodeBox:
5151
def __new__(
5252
cls,
53-
session_id: str | None = None,
54-
api_key: str | t.Literal["local", "docker"] | None = None,
55-
factory_id: str | t.Literal["default"] | None = None,
53+
session_id: t.Optional[str] = None,
54+
api_key: t.Optional[t.Union[str, t.Literal["local", "docker"]]] = None,
55+
factory_id: t.Optional[t.Union[str, t.Literal["default"]]] = None,
5656
) -> "CodeBox":
5757
"""
5858
Creates a CodeBox session
@@ -68,9 +68,9 @@ def __new__(
6868

6969
def __init__(
7070
self,
71-
session_id: str | None = None,
72-
api_key: str | t.Literal["local", "docker"] | None = None,
73-
factory_id: str | t.Literal["default"] | None = None,
71+
session_id: t.Optional[str] = None,
72+
api_key: t.Optional[t.Union[str, t.Literal["local", "docker"]]] = None,
73+
factory_id: t.Optional[t.Union[str, t.Literal["default"]]] = None,
7474
**_: bool,
7575
) -> None:
7676
self.session_id = session_id or "local"
@@ -81,37 +81,37 @@ def __init__(
8181

8282
def exec(
8383
self,
84-
code: str | os.PathLike,
84+
code: t.Union[str, os.PathLike],
8585
kernel: t.Literal["ipython", "bash"] = "ipython",
86-
timeout: float | None = None,
87-
cwd: str | None = None,
86+
timeout: t.Optional[float] = None,
87+
cwd: t.Optional[str] = None,
8888
) -> "ExecResult":
8989
"""Execute code inside the CodeBox instance"""
9090
return flatten_exec_result(self.stream_exec(code, kernel, timeout, cwd))
9191

9292
def stream_exec(
9393
self,
94-
code: str | os.PathLike,
94+
code: t.Union[str, os.PathLike],
9595
kernel: t.Literal["ipython", "bash"] = "ipython",
96-
timeout: float | None = None,
97-
cwd: str | None = None,
96+
timeout: t.Optional[float] = None,
97+
cwd: t.Optional[str] = None,
9898
) -> t.Generator["ExecChunk", None, None]:
9999
"""Executes the code and streams the result."""
100100
raise NotImplementedError("Abstract method, please use a subclass.")
101101

102102
def upload(
103103
self,
104104
remote_file_path: str,
105-
content: t.BinaryIO | bytes | str,
106-
timeout: float | None = None,
105+
content: t.Union[t.BinaryIO, bytes, str],
106+
timeout: t.Optional[float] = None,
107107
) -> "RemoteFile":
108108
"""Upload a file to the CodeBox instance"""
109109
raise NotImplementedError("Abstract method, please use a subclass.")
110110

111111
def stream_download(
112112
self,
113113
remote_file_path: str,
114-
timeout: float | None = None,
114+
timeout: t.Optional[float] = None,
115115
) -> t.Generator[bytes, None, None]:
116116
"""Download a file as open BinaryIO. Make sure to close the file after use."""
117117
raise NotImplementedError("Abstract method, please use a subclass.")
@@ -120,10 +120,10 @@ def stream_download(
120120

121121
async def aexec(
122122
self,
123-
code: str | os.PathLike,
123+
code: t.Union[str, os.PathLike],
124124
kernel: t.Literal["ipython", "bash"] = "ipython",
125-
timeout: float | None = None,
126-
cwd: str | None = None,
125+
timeout: t.Optional[float] = None,
126+
cwd: t.Optional[str] = None,
127127
) -> "ExecResult":
128128
"""Async Execute python code inside the CodeBox instance"""
129129
return await async_flatten_exec_result(
@@ -132,34 +132,34 @@ async def aexec(
132132

133133
def astream_exec(
134134
self,
135-
code: str | os.PathLike,
135+
code: t.Union[str, os.PathLike],
136136
kernel: t.Literal["ipython", "bash"] = "ipython",
137-
timeout: float | None = None,
138-
cwd: str | None = None,
137+
timeout: t.Optional[float] = None,
138+
cwd: t.Optional[str] = None,
139139
) -> t.AsyncGenerator["ExecChunk", None]:
140140
"""Async Stream Chunks of Execute python code inside the CodeBox instance"""
141141
raise NotImplementedError("Abstract method, please use a subclass.")
142142

143143
async def aupload(
144144
self,
145145
remote_file_path: str,
146-
content: t.BinaryIO | bytes | str,
147-
timeout: float | None = None,
146+
content: t.Union[t.BinaryIO, bytes, str],
147+
timeout: t.Optional[float] = None,
148148
) -> "RemoteFile":
149149
"""Async Upload a file to the CodeBox instance"""
150150
raise NotImplementedError("Abstract method, please use a subclass.")
151151

152152
async def adownload(
153153
self,
154154
remote_file_path: str,
155-
timeout: float | None = None,
155+
timeout: t.Optional[float] = None,
156156
) -> "RemoteFile":
157157
return [f for f in (await self.alist_files()) if f.path in remote_file_path][0]
158158

159159
def astream_download(
160160
self,
161161
remote_file_path: str,
162-
timeout: float | None = None,
162+
timeout: t.Optional[float] = None,
163163
) -> t.AsyncGenerator[bytes, None]:
164164
"""Async Download a file as BinaryIO. Make sure to close the file after use."""
165165
raise NotImplementedError("Abstract method, please use a subclass.")
@@ -242,7 +242,7 @@ async def ping(cb: CodeBox, d: int) -> None:
242242
# SYNCIFY
243243

244244
def download(
245-
self, remote_file_path: str, timeout: float | None = None
245+
self, remote_file_path: str, timeout: t.Optional[float] = None
246246
) -> "RemoteFile":
247247
return syncify(self.adownload)(remote_file_path, timeout)
248248

@@ -288,7 +288,7 @@ async def astop(self) -> t.Literal["stopped"]:
288288
@deprecated(
289289
"The `.run` method is deprecated. Use `.exec` instead.",
290290
)
291-
async def arun(self, code: str | os.PathLike) -> "CodeBoxOutput":
291+
async def arun(self, code: t.Union[str, os.PathLike]) -> "CodeBoxOutput":
292292
from .types import CodeBoxOutput
293293

294294
exec_result = await self.aexec(code, kernel="ipython")
@@ -321,7 +321,7 @@ def stop(self) -> t.Literal["stopped"]:
321321
@deprecated(
322322
"The `.run` method is deprecated. Use `.exec` instead.",
323323
)
324-
def run(self, code: str | os.PathLike) -> "CodeBoxOutput":
324+
def run(self, code: t.Union[str, os.PathLike]) -> "CodeBoxOutput":
325325
return syncify(self.arun)(code)
326326

327327
@deprecated(

src/codeboxapi/docker.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import socket
22
import subprocess
33
import time
4+
import typing as t
45

56
import httpx
67

78
from .remote import RemoteBox
89

910

10-
def get_free_port(port_or_range: int | tuple[int, int]) -> int:
11+
def get_free_port(port_or_range: t.Union[int, t.Tuple[int, int]]) -> int:
1112
if isinstance(port_or_range, int):
1213
port = port_or_range
1314
else:
@@ -29,7 +30,7 @@ def get_free_port(port_or_range: int | tuple[int, int]) -> int:
2930
class DockerBox(RemoteBox):
3031
def __init__(
3132
self,
32-
port_or_range: int | tuple[int, int] = 8069,
33+
port_or_range: t.Union[int, t.Tuple[int, int]] = 8069,
3334
image: str = "shroominic/codebox:latest",
3435
timeout: float = 3, # minutes
3536
start_container: bool = True,

0 commit comments

Comments
 (0)