Skip to content

Commit ae9828b

Browse files
authored
refactor: Remove LocalClient and use HTTPClient for local Tesseracts as well (#27)
#### Relevant issue or PR Fix #5 Our original strategy for using docker exec in order to operate on local Tesseracts was not a good idea; for now, we will rely on HTTP for local Tesseracts as well. #### Description of changes * Remove `LocalClient` * Adapt tests #### Testing done Extended tests + debugger and the following script ```python import numpy as np from tesseract_core import Tesseract a = np.array([1.0, 2.0, 3.0]) b = np.array([4.0, 5.0, 6.0]) with Tesseract.from_image(image="vectoradd") as vectoradd: vectoradd.apply({"a": a, "b": b}) ``` #### License - [x] By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license](https://pasteurlabs.github.io/tesseract/LICENSE). - [x] I sign the Developer Certificate of Origin below by adding my name and email address to the `Signed-off-by` line. <details> <summary><b>Developer Certificate of Origin</b></summary> ```text Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` </details> Signed-off-by: Alessandro Angioi <[email protected]>
1 parent efabdef commit ae9828b

File tree

2 files changed

+32
-100
lines changed

2 files changed

+32
-100
lines changed

tesseract_core/sdk/tesseract.py

Lines changed: 21 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ class Tesseract:
2626
instances spawned when instantiating the class).
2727
"""
2828

29-
_client: HTTPClient | LocalClient
3029
image: str
3130
volumes: list[str] | None
3231
gpus: list[str] | None
33-
project_id: str
32+
33+
_client: HTTPClient
34+
project_id: str | None = None
35+
container_id: str | None = None
3436

3537
def __init__(self, url: str) -> None:
3638
self._client = HTTPClient(url)
@@ -52,57 +54,45 @@ def from_image(
5254
return obj
5355

5456
def __enter__(self):
55-
if not self.client_type == "local":
56-
raise ValueError(
57-
"Use Tesseract.from_image(...) to create a context-managed Tesseract instance."
58-
)
59-
self._serve(volumes=self.volumes, gpus=self.gpus)
60-
self._client = LocalClient(self.tesseract_container_id)
57+
url = self._serve(volumes=self.volumes, gpus=self.gpus)
58+
self._client = HTTPClient(url)
6159
return self
6260

6361
def __exit__(self, exc_type, exc_value, traceback):
6462
engine.teardown(self.project_id)
63+
self.project_id = None
64+
self.container_id = None
6565

6666
def _serve(
6767
self,
6868
port: str = "",
6969
volumes: list[str] | None = None,
7070
gpus: list[str] | None = None,
71-
) -> None:
72-
if hasattr(self, "tesseract_container_id"):
73-
self.tesseract_container_id: str
71+
) -> str:
72+
if self.container_id:
7473
raise RuntimeError(
75-
"Client already attached to the Tesseract "
76-
f"container {self.tesseract_container_id}"
74+
"Client already attached to the Tesseract container {self.container_id}"
7775
)
7876
project_id = engine.serve([self.image], port=port, volumes=volumes, gpus=gpus)
7977

8078
command = ["docker", "compose", "-p", project_id, "ps", "--format", "json"]
8179
result = subprocess.run(command, capture_output=True, text=True)
8280

83-
containers = json.loads(result.stdout)
81+
# This relies on the fact that result.stdout from docker compose ps
82+
# contains multiple json dicts, one for each container, separated by newlines,
83+
# but json.loads will only parse the first one.
84+
# The first_container dict contains useful info like container id, ports, etc.
85+
first_container = json.loads(result.stdout)
8486

85-
if containers:
86-
first_container_id = containers["ID"]
87+
if first_container:
88+
first_container_id = first_container["ID"]
89+
first_container_port = first_container["Publishers"][0]["PublishedPort"]
8790
else:
8891
raise RuntimeError("No containers found.")
8992

90-
self.tesseract_container_id = first_container_id
9193
self.project_id = project_id
92-
93-
@cached_property
94-
def client_type(self) -> str:
95-
"""Get the type of client being used ('http' or 'local')."""
96-
return (
97-
"http"
98-
if hasattr(self, "_client") and isinstance(self._client, HTTPClient)
99-
else "local"
100-
)
101-
102-
@property
103-
def url(self) -> str | None:
104-
"""Get the URL if using HTTP client, None for local client."""
105-
return getattr(self._client, "url", None)
94+
self.container_id = first_container_id
95+
return f"http://localhost:{first_container_port}"
10696

10797
@cached_property
10898
def openapi_schema(self) -> dict:
@@ -351,46 +341,3 @@ def _run_tesseract(self, endpoint: str, payload: dict | None = None) -> dict:
351341
endpoint = "openapi.json"
352342

353343
return self._request(endpoint, method, payload)
354-
355-
356-
class LocalClient:
357-
"""A client that connects to a local Tesseract."""
358-
359-
def __init__(self, container_id: str) -> None:
360-
self.container_id = container_id
361-
362-
def _run_tesseract(
363-
self,
364-
endpoint: str,
365-
payload: dict | None = None,
366-
) -> dict:
367-
command = endpoint.replace("_", "-")
368-
if payload:
369-
encoded_payload = _tree_map(
370-
_encode_array, payload, is_leaf=lambda x: hasattr(x, "shape")
371-
)
372-
else:
373-
encoded_payload = None
374-
375-
args = []
376-
if encoded_payload:
377-
args.append(json.dumps(encoded_payload))
378-
379-
out, err = engine.exec_tesseract(self.container_id, command, args)
380-
381-
if err:
382-
raise RuntimeError(err)
383-
384-
data = json.loads(out)
385-
386-
if command in [
387-
"apply",
388-
"jacobian",
389-
"jacobian-vector-product",
390-
"vector-jacobian-product",
391-
]:
392-
data = _tree_map(
393-
_decode_array, data, is_leaf=lambda x: type(x) is dict and "shape" in x
394-
)
395-
396-
return data

tests/sdk_tests/test_tesseract.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from tesseract_core import Tesseract
55
from tesseract_core.sdk.tesseract import (
66
HTTPClient,
7-
LocalClient,
87
_decode_array,
98
_encode_array,
109
_tree_map,
@@ -17,7 +16,9 @@ def mock_serving(mocker):
1716
serve_mock.return_value = "proj-id-123"
1817

1918
subprocess_run_mock = mocker.patch("subprocess.run")
20-
subprocess_run_mock.return_value.stdout = '{"ID": "abc1234"}'
19+
subprocess_run_mock.return_value.stdout = (
20+
'{"ID": "abc1234", "Publishers":[{"PublishedPort": 54321}]}'
21+
)
2122

2223
teardown_mock = mocker.patch("tesseract_core.sdk.engine.teardown")
2324
return {
@@ -30,15 +31,12 @@ def mock_serving(mocker):
3031
@pytest.fixture
3132
def mock_clients(mocker):
3233
mocker.patch("tesseract_core.sdk.tesseract.HTTPClient._run_tesseract")
33-
mocker.patch("tesseract_core.sdk.tesseract.LocalClient._run_tesseract")
3434

3535

3636
def test_Tesseract_init():
3737
# Instantiate with a url
3838
t = Tesseract(url="localhost")
3939

40-
assert t.client_type == "http"
41-
4240
# The attributes for local Tesseracts should not be set
4341
assert not hasattr(t, "image")
4442
assert not hasattr(t, "gpus")
@@ -55,21 +53,18 @@ def test_Tesseract_from_image():
5553

5654
# Let's also check that stuff we don't expect there is not there
5755
assert not hasattr(t, "url")
58-
assert t.client_type == "local"
5956

6057

6158
def test_Tesseract_schema_methods(mocker, mock_serving):
62-
mocked_run = mocker.patch("tesseract_core.sdk.engine.exec_tesseract")
63-
mocked_run.return_value = '{"#defs": {"some": "stuff"}}', None
59+
mocked_run = mocker.patch("tesseract_core.sdk.tesseract.HTTPClient._run_tesseract")
60+
mocked_run.return_value = {"#defs": {"some": "stuff"}}
6461

6562
with Tesseract.from_image("sometesseract:0.2.3") as t:
6663
input_schema = t.input_schema
6764
output_schema = t.output_schema
6865
openapi_schema = t.openapi_schema
6966

70-
assert (
71-
input_schema == output_schema == openapi_schema == {"#defs": {"some": "stuff"}}
72-
)
67+
assert input_schema == output_schema == openapi_schema == mocked_run.return_value
7368

7469

7570
def test_serve_lifecycle(mock_serving, mock_clients):
@@ -84,21 +79,11 @@ def test_serve_lifecycle(mock_serving, mock_clients):
8479

8580
mock_serving["teardown_mock"].assert_called_with("proj-id-123")
8681

87-
88-
def test_LocalClient_exec(mocker):
89-
mocked_exec = mocker.patch("tesseract_core.sdk.engine.exec_tesseract")
90-
mocked_exec.return_value = '{"result": [4,4,4]}', None
91-
92-
client = LocalClient(container_id="1234567")
93-
94-
out = client._run_tesseract("apply", {"inputs": {"a": 1}})
95-
96-
assert out == {"result": [4, 4, 4]}
97-
mocked_exec.assert_called_with(
98-
"1234567",
99-
"apply",
100-
['{"inputs": {"a": 1}}'],
101-
)
82+
# check that the same Tesseract obj cannot be used to instantiate two containers
83+
with pytest.raises(RuntimeError):
84+
with t:
85+
with t:
86+
pass
10287

10388

10489
def test_HTTPClient_run_tesseract(mocker):

0 commit comments

Comments
 (0)