graph2table 0.1.0__tar.gz

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.
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 graph2table
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.5
2
+ Name: graph2table
3
+ Version: 0.1.0
4
+ Summary: Client for the graph2table API: send a chart image, get the data table.
5
+ Project-URL: Homepage, https://graph2table.com
6
+ Project-URL: Documentation, https://graph2table.com/docs/api
7
+ Project-URL: Benchmark, https://graph2table.com/blog/chartx-technical-report
8
+ Author: graph2table
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: chart,chart to table,csv,data extraction,graph,plot digitizer
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: requests>=2.28
18
+ Description-Content-Type: text/markdown
19
+
20
+ # graph2table
21
+
22
+ Python client for the [graph2table](https://graph2table.com) API. Send a chart image, get the data table.
23
+
24
+ graph2table automatically extracts the data table from a chart image.
25
+
26
+ ## Install
27
+
28
+ ```
29
+ pip install graph2table
30
+ ```
31
+
32
+ ## Use
33
+
34
+ ```python
35
+ from graph2table import convert
36
+
37
+ result = convert("figure.png", api_key="YOUR_API_KEY")
38
+
39
+ result.headers # ['x', 'Series A', 'Series B']
40
+ result.rows[0] # {'x': 0.0, 'Series A': 12.4, 'Series B': 9.1}
41
+ result.to_csv("figure.csv")
42
+ result.to_dataframe() # pandas, if installed
43
+ ```
44
+
45
+ Get an API key at https://graph2table.com/app/keys. Each call costs 10 credits. Set the `GRAPH2TABLE_API_KEY` environment variable to skip the `api_key` argument.
46
+
47
+ The image can be a file path, raw bytes, or an open file. PNG, JPG and WebP up to 10 MB.
48
+
49
+ To focus the extraction, pass `custom_instructions="Extract only the red series"`.
50
+
51
+ Errors raise `Graph2TableError` with the HTTP status and the API message.
52
+
53
+ ## Check the result
54
+
55
+ Open the same image at https://graph2table.com/converter to see every extracted point drawn on the chart, and correct a point before you export.
56
+
57
+ ## Links
58
+
59
+ - API reference: https://graph2table.com/docs/api
60
+ - Accuracy and released data: https://graph2table.com/blog/chartx-technical-report
@@ -0,0 +1,41 @@
1
+ # graph2table
2
+
3
+ Python client for the [graph2table](https://graph2table.com) API. Send a chart image, get the data table.
4
+
5
+ graph2table automatically extracts the data table from a chart image.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install graph2table
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```python
16
+ from graph2table import convert
17
+
18
+ result = convert("figure.png", api_key="YOUR_API_KEY")
19
+
20
+ result.headers # ['x', 'Series A', 'Series B']
21
+ result.rows[0] # {'x': 0.0, 'Series A': 12.4, 'Series B': 9.1}
22
+ result.to_csv("figure.csv")
23
+ result.to_dataframe() # pandas, if installed
24
+ ```
25
+
26
+ Get an API key at https://graph2table.com/app/keys. Each call costs 10 credits. Set the `GRAPH2TABLE_API_KEY` environment variable to skip the `api_key` argument.
27
+
28
+ The image can be a file path, raw bytes, or an open file. PNG, JPG and WebP up to 10 MB.
29
+
30
+ To focus the extraction, pass `custom_instructions="Extract only the red series"`.
31
+
32
+ Errors raise `Graph2TableError` with the HTTP status and the API message.
33
+
34
+ ## Check the result
35
+
36
+ Open the same image at https://graph2table.com/converter to see every extracted point drawn on the chart, and correct a point before you export.
37
+
38
+ ## Links
39
+
40
+ - API reference: https://graph2table.com/docs/api
41
+ - Accuracy and released data: https://graph2table.com/blog/chartx-technical-report
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "graph2table"
7
+ version = "0.1.0"
8
+ description = "Client for the graph2table API: send a chart image, get the data table."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "graph2table" }]
14
+ keywords = ["chart", "graph", "plot digitizer", "data extraction", "chart to table", "csv"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Scientific/Engineering",
20
+ ]
21
+ dependencies = ["requests>=2.28"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://graph2table.com"
25
+ Documentation = "https://graph2table.com/docs/api"
26
+ Benchmark = "https://graph2table.com/blog/chartx-technical-report"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/graph2table"]
@@ -0,0 +1,14 @@
1
+ """graph2table Python client. Send a chart image, get the data table.
2
+
3
+ from graph2table import convert
4
+
5
+ result = convert("figure.png", api_key="YOUR_API_KEY")
6
+ result.headers
7
+ result.rows
8
+ result.to_csv("figure.csv")
9
+ """
10
+
11
+ from .client import Client, ConversionResult, Graph2TableError, convert
12
+
13
+ __all__ = ["Client", "ConversionResult", "Graph2TableError", "convert"]
14
+ __version__ = "0.1.0"
@@ -0,0 +1,155 @@
1
+ """HTTP client for the graph2table API (POST /api/v1/convert)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import io
7
+ import json
8
+ import os
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import IO, Any, Dict, List, Optional, Union
12
+
13
+ import requests
14
+
15
+ DEFAULT_BASE_URL = "https://graph2table.com"
16
+ DEFAULT_TIMEOUT = 180
17
+ ENV_API_KEY = "GRAPH2TABLE_API_KEY"
18
+
19
+ ImageInput = Union[str, "os.PathLike[str]", bytes, IO[bytes]]
20
+
21
+
22
+ class Graph2TableError(Exception):
23
+ """Raised when the API returns an error response."""
24
+
25
+ def __init__(self, status: int, message: str):
26
+ super().__init__(f"graph2table API error {status}: {message}")
27
+ self.status = status
28
+ self.message = message
29
+
30
+
31
+ @dataclass
32
+ class ConversionResult:
33
+ """The extracted table. `rows` are dicts keyed by column header."""
34
+
35
+ headers: List[str]
36
+ rows: List[Dict[str, Any]] = field(default_factory=list)
37
+
38
+ def to_csv(self, path: Optional[Union[str, "os.PathLike[str]"]] = None) -> str:
39
+ """Return the table as CSV text. If `path` is given, also write it there."""
40
+ buf = io.StringIO()
41
+ writer = csv.DictWriter(buf, fieldnames=self.headers, extrasaction="ignore")
42
+ writer.writeheader()
43
+ for row in self.rows:
44
+ writer.writerow(row)
45
+ text = buf.getvalue()
46
+ if path is not None:
47
+ Path(path).write_text(text, encoding="utf-8", newline="")
48
+ return text
49
+
50
+ def to_dataframe(self): # type: ignore[no-untyped-def]
51
+ """Return a pandas DataFrame (pandas is an optional dependency)."""
52
+ try:
53
+ import pandas as pd
54
+ except ImportError as exc: # pragma: no cover
55
+ raise ImportError("pandas is required: pip install graph2table[pandas]") from exc
56
+ return pd.DataFrame(self.rows, columns=self.headers)
57
+
58
+ def __len__(self) -> int:
59
+ return len(self.rows)
60
+
61
+
62
+ def _parse_data(data: Any) -> ConversionResult:
63
+ """The API returns `data` as a JSON string holding a list of row objects."""
64
+ rows = json.loads(data) if isinstance(data, str) else data
65
+ if not isinstance(rows, list):
66
+ raise Graph2TableError(200, "Unexpected response shape: data is not a list")
67
+ headers: List[str] = []
68
+ for row in rows:
69
+ for key in row:
70
+ if key not in headers:
71
+ headers.append(key)
72
+ return ConversionResult(headers=headers, rows=rows)
73
+
74
+
75
+ def _open_image(image: ImageInput, filename: Optional[str]):
76
+ """Return (filename, bytes-or-file) for the multipart upload."""
77
+ if isinstance(image, (bytes, bytearray)):
78
+ return filename or "image.png", bytes(image)
79
+ if hasattr(image, "read"):
80
+ name = filename or getattr(image, "name", None) or "image.png"
81
+ return os.path.basename(str(name)), image
82
+ path = Path(image)
83
+ return filename or path.name, path.read_bytes()
84
+
85
+
86
+ class Client:
87
+ """Client for the graph2table API.
88
+
89
+ The API key comes from the `api_key` argument or the GRAPH2TABLE_API_KEY
90
+ environment variable. Create keys at https://graph2table.com/app/keys.
91
+ Every request costs 10 credits.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ api_key: Optional[str] = None,
97
+ base_url: str = DEFAULT_BASE_URL,
98
+ timeout: float = DEFAULT_TIMEOUT,
99
+ session: Optional[requests.Session] = None,
100
+ ):
101
+ key = api_key or os.environ.get(ENV_API_KEY)
102
+ if not key:
103
+ raise ValueError(
104
+ f"No API key. Pass api_key=... or set the {ENV_API_KEY} environment variable."
105
+ )
106
+ self.api_key = key
107
+ self.base_url = base_url.rstrip("/")
108
+ self.timeout = timeout
109
+ self.session = session or requests.Session()
110
+
111
+ def convert(
112
+ self,
113
+ image: ImageInput,
114
+ custom_instructions: Optional[str] = None,
115
+ filename: Optional[str] = None,
116
+ ) -> ConversionResult:
117
+ """Extract the data table from one chart image.
118
+
119
+ `image` is a file path, raw bytes, or an open binary file. PNG, JPG and
120
+ WebP up to 10 MB are accepted. `custom_instructions` (500 characters or
121
+ fewer) tells the extractor what to focus on, for example
122
+ "Extract only the red series".
123
+ """
124
+ name, payload = _open_image(image, filename)
125
+ files = {"file": (name, payload)}
126
+ data = {}
127
+ if custom_instructions:
128
+ data["custom_instructions"] = custom_instructions
129
+ response = self.session.post(
130
+ f"{self.base_url}/api/v1/convert",
131
+ headers={"Authorization": f"Bearer {self.api_key}"},
132
+ files=files,
133
+ data=data,
134
+ timeout=self.timeout,
135
+ )
136
+ try:
137
+ body = response.json()
138
+ except ValueError:
139
+ body = {}
140
+ if response.status_code != 200 or not body.get("success"):
141
+ message = body.get("error") if isinstance(body, dict) else None
142
+ raise Graph2TableError(response.status_code, message or response.text[:200])
143
+ return _parse_data(body.get("data"))
144
+
145
+
146
+ def convert(
147
+ image: ImageInput,
148
+ api_key: Optional[str] = None,
149
+ custom_instructions: Optional[str] = None,
150
+ **client_kwargs: Any,
151
+ ) -> ConversionResult:
152
+ """One-call convenience wrapper around `Client.convert`."""
153
+ return Client(api_key=api_key, **client_kwargs).convert(
154
+ image, custom_instructions=custom_instructions
155
+ )
@@ -0,0 +1,79 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from graph2table import Client, Graph2TableError, convert
6
+ from graph2table.client import _parse_data
7
+
8
+
9
+ class FakeResponse:
10
+ def __init__(self, status_code, body, text=""):
11
+ self.status_code = status_code
12
+ self._body = body
13
+ self.text = text or json.dumps(body)
14
+
15
+ def json(self):
16
+ return self._body
17
+
18
+
19
+ class FakeSession:
20
+ def __init__(self, response):
21
+ self.response = response
22
+ self.calls = []
23
+
24
+ def post(self, url, **kwargs):
25
+ self.calls.append((url, kwargs))
26
+ return self.response
27
+
28
+
29
+ def legacy_data(rows):
30
+ # The API returns `data` as a pretty-printed JSON string of row objects.
31
+ return json.dumps(rows, indent=4)
32
+
33
+
34
+ def test_convert_parses_legacy_data(tmp_path):
35
+ rows = [{"x": 1, "Series A": 2.5}, {"x": 2, "Series A": 3.0}]
36
+ session = FakeSession(FakeResponse(200, {"success": True, "data": legacy_data(rows)}))
37
+ img = tmp_path / "chart.png"
38
+ img.write_bytes(b"\x89PNG fake")
39
+
40
+ result = Client(api_key="k", session=session).convert(img, custom_instructions="red only")
41
+
42
+ assert result.headers == ["x", "Series A"]
43
+ assert result.rows == rows
44
+ assert len(result) == 2
45
+ url, kwargs = session.calls[0]
46
+ assert url == "https://graph2table.com/api/v1/convert"
47
+ assert kwargs["headers"]["Authorization"] == "Bearer k"
48
+ assert kwargs["files"]["file"][0] == "chart.png"
49
+ assert kwargs["data"] == {"custom_instructions": "red only"}
50
+
51
+
52
+ def test_to_csv_writes_file(tmp_path):
53
+ result = _parse_data(legacy_data([{"x": 1, "y": 2}, {"x": 3, "y": 4}]))
54
+ out = tmp_path / "t.csv"
55
+ text = result.to_csv(out)
56
+ assert text.splitlines() == ["x,y", "1,2", "3,4"]
57
+ assert out.read_text(encoding="utf-8").splitlines() == ["x,y", "1,2", "3,4"]
58
+
59
+
60
+ def test_error_response_raises():
61
+ session = FakeSession(FakeResponse(402, {"error": "Insufficient credits."}))
62
+ with pytest.raises(Graph2TableError) as exc:
63
+ Client(api_key="k", session=session).convert(b"bytes")
64
+ assert exc.value.status == 402
65
+ assert "Insufficient credits" in str(exc.value)
66
+
67
+
68
+ def test_missing_api_key(monkeypatch):
69
+ monkeypatch.delenv("GRAPH2TABLE_API_KEY", raising=False)
70
+ with pytest.raises(ValueError):
71
+ Client()
72
+
73
+
74
+ def test_env_api_key(monkeypatch):
75
+ monkeypatch.setenv("GRAPH2TABLE_API_KEY", "from-env")
76
+ session = FakeSession(FakeResponse(200, {"success": True, "data": "[]"}))
77
+ result = convert(b"bytes", session=session)
78
+ assert result.rows == []
79
+ assert session.calls[0][1]["headers"]["Authorization"] == "Bearer from-env"