augplot 0.1.0__tar.gz → 0.1.1__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.
- {augplot-0.1.0 → augplot-0.1.1}/PKG-INFO +36 -2
- {augplot-0.1.0 → augplot-0.1.1}/README.md +35 -1
- {augplot-0.1.0 → augplot-0.1.1}/docs/api.md +3 -2
- {augplot-0.1.0 → augplot-0.1.1}/docs/generated-code-guardrails.md +19 -6
- {augplot-0.1.0 → augplot-0.1.1}/pyproject.toml +1 -1
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/__init__.py +1 -1
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/execution.py +203 -26
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/profiling.py +6 -2
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/prompts.py +21 -3
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_execution.py +351 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_profiling.py +18 -2
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_prompts.py +8 -1
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_workflow.py +72 -0
- augplot-0.1.1/tests/titanic_fixture.py +40 -0
- {augplot-0.1.0 → augplot-0.1.1}/.github/dependabot.yml +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/.github/workflows/codeql.yml +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/.github/workflows/release.yml +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/.github/workflows/tests.yml +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/.gitignore +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/AGENTS.md +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/CHANGELOG.md +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/CODE_OF_CONDUCT.md +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/CONTRIBUTING.md +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/LICENSE +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/NOTICE +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/SECURITY.md +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/docs/assets/augplot-workflow.png +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/docs/visualization-history.md +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/examples/quickstart.ipynb +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/core.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/datasets.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/errors.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/exporting.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/history.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/src/augplot/provider.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/conftest.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/notebook_charts.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_datasets.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_display.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_export.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_history.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_live.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_metadata.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_notebook.py +0 -0
- {augplot-0.1.0 → augplot-0.1.1}/tests/test_provider.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: augplot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Turn notebook data into visualizations and reusable Python with an LLM.
|
|
5
5
|
Project-URL: Repository, https://github.com/egpand/augplot
|
|
6
6
|
Project-URL: Documentation, https://github.com/egpand/augplot/tree/main/docs
|
|
@@ -78,6 +78,40 @@ viz.to_python(function_name="plot_monthly_passengers")
|
|
|
78
78
|
Open or download the [example notebook](examples/quickstart.ipynb) to try the complete
|
|
79
79
|
workflow.
|
|
80
80
|
|
|
81
|
+
## What can `data` be?
|
|
82
|
+
|
|
83
|
+
`ap.plot(data)` accepts these nonempty, in-memory Python objects:
|
|
84
|
+
|
|
85
|
+
| Input | Typical use |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `pandas.DataFrame` | Rows and columns, including dates and categories. |
|
|
88
|
+
| `pandas.Series` | One labeled sequence of values. |
|
|
89
|
+
| `numpy.ndarray` | One- or two-dimensional numeric or mixed data. |
|
|
90
|
+
| `list` or `tuple` | Values such as `[2, 4, 6]`, or records such as `[{'x': 1, 'y': 2}]`. |
|
|
91
|
+
| `dict` | Columns such as `{'x': [1, 2], 'y': [2, 3]}`, or nested results keyed by model and metric. |
|
|
92
|
+
|
|
93
|
+
Nested lists and dictionaries can contain ordinary scalars, missing values, NumPy arrays,
|
|
94
|
+
and Pandas objects. Parsed JSON is therefore a valid input when its root is a nonempty
|
|
95
|
+
list or dictionary:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import json
|
|
99
|
+
|
|
100
|
+
records = json.loads('[{"month": "Jan", "sales": 10}, {"month": "Feb", "sales": 12}]')
|
|
101
|
+
viz = ap.plot(records)
|
|
102
|
+
|
|
103
|
+
results = {
|
|
104
|
+
"baseline": {"test_f1": [0.3, 0.4, 0.5]},
|
|
105
|
+
"candidate": {"test_f1": [0.4, 0.5, 0.6]},
|
|
106
|
+
}
|
|
107
|
+
viz = ap.plot(results, prompt="Compare F1 scores across models")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pass the parsed object, not a JSON string or file path. Empty inputs, cyclic containers,
|
|
111
|
+
custom objects, and NumPy arrays with zero or more than two dimensions raise `DataError`.
|
|
112
|
+
For a Pandas `Index`, another table library, or an iterator, convert to one of the types
|
|
113
|
+
above first.
|
|
114
|
+
|
|
81
115
|
## Workflow
|
|
82
116
|
|
|
83
117
|
- `plot()` generates the initial visualization.
|
|
@@ -139,7 +173,7 @@ but does not train models or return predictions, forecasts, or other analytical
|
|
|
139
173
|
|
|
140
174
|
## Beta and security
|
|
141
175
|
|
|
142
|
-
Augplot 0.1.
|
|
176
|
+
Augplot 0.1.1 is a beta release; APIs and saved-history formats may change before 1.0.
|
|
143
177
|
Review generated code before sensitive or security-critical use, and report
|
|
144
178
|
vulnerabilities through the [security policy](SECURITY.md).
|
|
145
179
|
|
|
@@ -40,6 +40,40 @@ viz.to_python(function_name="plot_monthly_passengers")
|
|
|
40
40
|
Open or download the [example notebook](examples/quickstart.ipynb) to try the complete
|
|
41
41
|
workflow.
|
|
42
42
|
|
|
43
|
+
## What can `data` be?
|
|
44
|
+
|
|
45
|
+
`ap.plot(data)` accepts these nonempty, in-memory Python objects:
|
|
46
|
+
|
|
47
|
+
| Input | Typical use |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `pandas.DataFrame` | Rows and columns, including dates and categories. |
|
|
50
|
+
| `pandas.Series` | One labeled sequence of values. |
|
|
51
|
+
| `numpy.ndarray` | One- or two-dimensional numeric or mixed data. |
|
|
52
|
+
| `list` or `tuple` | Values such as `[2, 4, 6]`, or records such as `[{'x': 1, 'y': 2}]`. |
|
|
53
|
+
| `dict` | Columns such as `{'x': [1, 2], 'y': [2, 3]}`, or nested results keyed by model and metric. |
|
|
54
|
+
|
|
55
|
+
Nested lists and dictionaries can contain ordinary scalars, missing values, NumPy arrays,
|
|
56
|
+
and Pandas objects. Parsed JSON is therefore a valid input when its root is a nonempty
|
|
57
|
+
list or dictionary:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import json
|
|
61
|
+
|
|
62
|
+
records = json.loads('[{"month": "Jan", "sales": 10}, {"month": "Feb", "sales": 12}]')
|
|
63
|
+
viz = ap.plot(records)
|
|
64
|
+
|
|
65
|
+
results = {
|
|
66
|
+
"baseline": {"test_f1": [0.3, 0.4, 0.5]},
|
|
67
|
+
"candidate": {"test_f1": [0.4, 0.5, 0.6]},
|
|
68
|
+
}
|
|
69
|
+
viz = ap.plot(results, prompt="Compare F1 scores across models")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Pass the parsed object, not a JSON string or file path. Empty inputs, cyclic containers,
|
|
73
|
+
custom objects, and NumPy arrays with zero or more than two dimensions raise `DataError`.
|
|
74
|
+
For a Pandas `Index`, another table library, or an iterator, convert to one of the types
|
|
75
|
+
above first.
|
|
76
|
+
|
|
43
77
|
## Workflow
|
|
44
78
|
|
|
45
79
|
- `plot()` generates the initial visualization.
|
|
@@ -101,7 +135,7 @@ but does not train models or return predictions, forecasts, or other analytical
|
|
|
101
135
|
|
|
102
136
|
## Beta and security
|
|
103
137
|
|
|
104
|
-
Augplot 0.1.
|
|
138
|
+
Augplot 0.1.1 is a beta release; APIs and saved-history formats may change before 1.0.
|
|
105
139
|
Review generated code before sensitive or security-critical use, and report
|
|
106
140
|
vulnerabilities through the [security policy](SECURITY.md).
|
|
107
141
|
|
|
@@ -11,8 +11,9 @@ import augplot as ap
|
|
|
11
11
|
plt = ap.plot(data, prompt="Plot revenue by channel")
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`ap.plot()` accepts Pandas objects, NumPy arrays, lists,
|
|
15
|
-
returns the current Augplot chart object and displays its
|
|
14
|
+
`ap.plot()` accepts Pandas objects, one- or two-dimensional NumPy arrays, lists, tuples,
|
|
15
|
+
and nested dictionaries. It returns the current Augplot chart object and displays its
|
|
16
|
+
figure by default.
|
|
16
17
|
|
|
17
18
|
```python
|
|
18
19
|
plt = ap.plot(
|
|
@@ -10,15 +10,27 @@ Generated code must define exactly `plot_data(data, *, title=None, figsize=None)
|
|
|
10
10
|
return a figure it created. It may use the approved, in-memory parts of NumPy, Pandas,
|
|
11
11
|
Matplotlib, and Seaborn. This includes common chart types, subplot layouts, axes, legends,
|
|
12
12
|
artists, ticks, date formatters, and supported data transformations.
|
|
13
|
+
Nested dictionaries and JSON-like records remain valid inputs. For cross-validation
|
|
14
|
+
results keyed by model name, generated code can derive values with a single comprehension,
|
|
15
|
+
such as `[np.mean(data[name]["test_f1"]) for name in data]`.
|
|
13
16
|
|
|
14
17
|
Imports use fixed aliases: `np`, `pd`, `plt`, `ticker`, `dates`, and `sns`.
|
|
15
18
|
Every call and attribute path is checked, and figures, axes, artists, and data-derived
|
|
16
19
|
values are tracked so that a valid object cannot be substituted with an arbitrary callable.
|
|
17
20
|
|
|
18
|
-
Loops are limited to approved Axes collections, small static sequences, or
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
loops.
|
|
21
|
+
Loops are limited to approved Axes collections, small static sequences, or data explicitly
|
|
22
|
+
bounded to at most 200 rows with `head` or `tail`. Bounded columns and
|
|
23
|
+
`data.head(20).iterrows()` support per-record chart annotations. Unbounded row iteration
|
|
24
|
+
and nested generated loops remain rejected.
|
|
25
|
+
One-dimensional Axes collections from a fixed-size `subplots` call can be iterated directly.
|
|
26
|
+
A list slice such as `names = list(data)[:200]` also provides a bounded sequence; a
|
|
27
|
+
matching `np.arange(len(names))` may position its marks.
|
|
28
|
+
An array extent stored in a local name, such as `column_count = values.shape[1]`, may
|
|
29
|
+
also be used to position marks with `np.arange(column_count)`; it does not authorize an
|
|
30
|
+
unbounded Python annotation loop.
|
|
31
|
+
Simple layout values can be assigned in both branches of a conditional, including a
|
|
32
|
+
default `figsize`. A bounded loop over the four Matplotlib spines may hide them for
|
|
33
|
+
minimal timeline styling.
|
|
22
34
|
|
|
23
35
|
The generation prompt summarizes the validator's main constraints so a provider is less
|
|
24
36
|
likely to emit code that needs repair. This is compatibility guidance only: the prompt is
|
|
@@ -45,14 +57,15 @@ plt.imsave("chart.png", data)
|
|
|
45
57
|
|
|
46
58
|
Fresh model output receives at most the configured repair attempt (`max_repairs=1` by
|
|
47
59
|
default). The repair request contains a sanitized validation diagnostic, never a runtime
|
|
48
|
-
exception message or data values.
|
|
60
|
+
exception message or data values. A recognized scatter length mismatch produces a
|
|
61
|
+
data-free hint so the model can repair the x and y arrays. If it still fails, `ap.plot()` raises
|
|
49
62
|
`GenerationError`; rejected source is never executed, displayed, exported, or saved.
|
|
50
63
|
|
|
51
64
|
```python
|
|
52
65
|
try:
|
|
53
66
|
viz = ap.plot(data, prompt="...")
|
|
54
67
|
except ap.GenerationError as exc:
|
|
55
|
-
print(exc) #
|
|
68
|
+
print(exc) # reason, with the rejected name or method and line when known
|
|
56
69
|
print(exc.violations) # structured validation failures
|
|
57
70
|
print(exc.code) # inspect only when appropriate
|
|
58
71
|
```
|
|
@@ -8,7 +8,7 @@ from contextlib import ExitStack
|
|
|
8
8
|
from .errors import GenerationError, ScopeError
|
|
9
9
|
from .profiling import copy_data
|
|
10
10
|
|
|
11
|
-
API_MANIFEST_VERSION =
|
|
11
|
+
API_MANIFEST_VERSION = 8
|
|
12
12
|
MAX_SOURCE_CHARS = 50_000
|
|
13
13
|
MAX_AST_NODES = 4_000
|
|
14
14
|
MAX_LITERAL_ITEMS = 2_000
|
|
@@ -176,6 +176,7 @@ _DATA_METHODS = {
|
|
|
176
176
|
"idxmin",
|
|
177
177
|
"infer_objects",
|
|
178
178
|
"interpolate",
|
|
179
|
+
"iterrows",
|
|
179
180
|
"isin",
|
|
180
181
|
"isna",
|
|
181
182
|
"items",
|
|
@@ -445,6 +446,7 @@ _AXES = {
|
|
|
445
446
|
"secondary_yaxis",
|
|
446
447
|
"set",
|
|
447
448
|
"set_aspect",
|
|
449
|
+
"set_axisbelow",
|
|
448
450
|
"set_axis_off",
|
|
449
451
|
"set_axis_on",
|
|
450
452
|
"set_box_aspect",
|
|
@@ -456,10 +458,12 @@ _AXES = {
|
|
|
456
458
|
"set_xlim",
|
|
457
459
|
"set_xscale",
|
|
458
460
|
"set_xticks",
|
|
461
|
+
"set_xticklabels",
|
|
459
462
|
"set_ylabel",
|
|
460
463
|
"set_ylim",
|
|
461
464
|
"set_yscale",
|
|
462
465
|
"set_yticks",
|
|
466
|
+
"set_yticklabels",
|
|
463
467
|
"sharex",
|
|
464
468
|
"sharey",
|
|
465
469
|
"specgram",
|
|
@@ -612,11 +616,18 @@ class _Validator:
|
|
|
612
616
|
|
|
613
617
|
def merge_branches(self, before, body, otherwise):
|
|
614
618
|
merged = {}
|
|
619
|
+
passive_kinds = {"value", "data", "bounded_sequence"}
|
|
615
620
|
for name in before.keys() | body.keys() | otherwise.keys():
|
|
616
621
|
body_kind = body.get(name)
|
|
617
622
|
otherwise_kind = otherwise.get(name)
|
|
618
623
|
if body_kind == otherwise_kind and body_kind is not None:
|
|
619
624
|
merged[name] = body_kind
|
|
625
|
+
self.invalid_provenance.discard(name)
|
|
626
|
+
elif {body_kind, otherwise_kind} <= passive_kinds:
|
|
627
|
+
# Keep ordinary values usable after a branch, without carrying a
|
|
628
|
+
# bounded-iteration proof or a plotting-object capability across it.
|
|
629
|
+
merged[name] = "data"
|
|
630
|
+
self.invalid_provenance.discard(name)
|
|
620
631
|
else:
|
|
621
632
|
self.invalid_provenance.add(name)
|
|
622
633
|
return merged
|
|
@@ -695,6 +706,108 @@ class _Validator:
|
|
|
695
706
|
except (TypeError, ValueError, OverflowError):
|
|
696
707
|
return False
|
|
697
708
|
|
|
709
|
+
def bounded_slice(self, node):
|
|
710
|
+
if not isinstance(node, ast.Slice) or node.upper is None:
|
|
711
|
+
return False
|
|
712
|
+
lower = 0 if node.lower is None else self.static_integer(node.lower)
|
|
713
|
+
upper = self.static_integer(node.upper)
|
|
714
|
+
step = 1 if node.step is None else self.static_integer(node.step)
|
|
715
|
+
return (
|
|
716
|
+
lower is not None
|
|
717
|
+
and upper is not None
|
|
718
|
+
and step is not None
|
|
719
|
+
and 0 <= lower <= upper
|
|
720
|
+
and 0 < step
|
|
721
|
+
and upper - lower <= MAX_BOUNDED_ITERATION
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def bounded_arange(self, node):
|
|
725
|
+
if len(node.args) != 1 or node.keywords:
|
|
726
|
+
return False
|
|
727
|
+
stop = node.args[0]
|
|
728
|
+
return (
|
|
729
|
+
isinstance(stop, ast.Call)
|
|
730
|
+
and isinstance(stop.func, ast.Name)
|
|
731
|
+
and stop.func.id == "len"
|
|
732
|
+
and len(stop.args) == 1
|
|
733
|
+
and not stop.keywords
|
|
734
|
+
and isinstance(stop.args[0], ast.Name)
|
|
735
|
+
and self.env.get(stop.args[0].id)
|
|
736
|
+
in {"bounded_column", "bounded_data", "bounded_sequence"}
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def data_extent(self, node):
|
|
740
|
+
data_kinds = {"bounded_column", "bounded_data", "bounded_sequence", "data", "sequence"}
|
|
741
|
+
if isinstance(node, ast.Name):
|
|
742
|
+
return self.env.get(node.id) == "data_extent"
|
|
743
|
+
if isinstance(node, ast.Call):
|
|
744
|
+
return (
|
|
745
|
+
isinstance(node.func, ast.Name)
|
|
746
|
+
and node.func.id == "len"
|
|
747
|
+
and len(node.args) == 1
|
|
748
|
+
and not node.keywords
|
|
749
|
+
and isinstance(node.args[0], ast.Name)
|
|
750
|
+
and self.env.get(node.args[0].id) in data_kinds
|
|
751
|
+
)
|
|
752
|
+
if isinstance(node, ast.Attribute):
|
|
753
|
+
return (
|
|
754
|
+
node.attr == "size"
|
|
755
|
+
and isinstance(node.value, ast.Name)
|
|
756
|
+
and self.env.get(node.value.id) in data_kinds
|
|
757
|
+
)
|
|
758
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Attribute):
|
|
759
|
+
return (
|
|
760
|
+
node.value.attr == "shape"
|
|
761
|
+
and isinstance(node.value.value, ast.Name)
|
|
762
|
+
and self.env.get(node.value.value.id) in data_kinds
|
|
763
|
+
and isinstance(node.slice, ast.Constant)
|
|
764
|
+
and node.slice.value in {0, 1}
|
|
765
|
+
)
|
|
766
|
+
return False
|
|
767
|
+
|
|
768
|
+
def data_sized_stop(self, node):
|
|
769
|
+
if self.data_extent(node):
|
|
770
|
+
return True
|
|
771
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub)):
|
|
772
|
+
return (
|
|
773
|
+
self.data_extent(node.left)
|
|
774
|
+
and isinstance(node.right, ast.Constant)
|
|
775
|
+
and isinstance(node.right.value, int)
|
|
776
|
+
and abs(node.right.value) <= 1
|
|
777
|
+
)
|
|
778
|
+
return False
|
|
779
|
+
|
|
780
|
+
def data_sized_range(self, node):
|
|
781
|
+
if node.keywords or len(node.args) not in {1, 2}:
|
|
782
|
+
return False
|
|
783
|
+
if len(node.args) == 1:
|
|
784
|
+
return self.data_sized_stop(node.args[0])
|
|
785
|
+
start = self.static_integer(node.args[0])
|
|
786
|
+
return start in {0, 1} and self.data_sized_stop(node.args[1])
|
|
787
|
+
|
|
788
|
+
def subplot_result(self, node):
|
|
789
|
+
rows = self.keyword_value(node, "nrows")
|
|
790
|
+
columns = self.keyword_value(node, "ncols")
|
|
791
|
+
if rows is None and node.args:
|
|
792
|
+
rows = node.args[0]
|
|
793
|
+
if columns is None and len(node.args) > 1:
|
|
794
|
+
columns = node.args[1]
|
|
795
|
+
rows = 1 if rows is None else self.static_integer(rows)
|
|
796
|
+
columns = 1 if columns is None else self.static_integer(columns)
|
|
797
|
+
squeeze = self.keyword_value(node, "squeeze")
|
|
798
|
+
is_flat_axes = (
|
|
799
|
+
isinstance(rows, int)
|
|
800
|
+
and isinstance(columns, int)
|
|
801
|
+
and 0 < rows * columns <= 100
|
|
802
|
+
and rows * columns > 1
|
|
803
|
+
and (rows == 1 or columns == 1)
|
|
804
|
+
and (
|
|
805
|
+
squeeze is None
|
|
806
|
+
or (isinstance(squeeze, ast.Constant) and squeeze.value is True)
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
return "axes_sequence" if is_flat_axes else "axes"
|
|
810
|
+
|
|
698
811
|
def keyword_value(self, node, name):
|
|
699
812
|
return next((item.value for item in node.keywords if item.arg == name), None)
|
|
700
813
|
|
|
@@ -822,7 +935,10 @@ class _Validator:
|
|
|
822
935
|
)
|
|
823
936
|
if self.loop_depth:
|
|
824
937
|
self.fail("resource_limit", "Nested loops are not allowed.")
|
|
825
|
-
|
|
938
|
+
target_kind = kind if kind == "axes_sequence" else "value"
|
|
939
|
+
if kind == "axes_sequence" and isinstance(n.target, ast.Name):
|
|
940
|
+
target_kind = "axes"
|
|
941
|
+
self.assign(n.target, target_kind)
|
|
826
942
|
self.loop_depth += 1
|
|
827
943
|
try:
|
|
828
944
|
for s in n.body + n.orelse:
|
|
@@ -849,6 +965,7 @@ class _Validator:
|
|
|
849
965
|
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
850
966
|
if kind not in {
|
|
851
967
|
"figure_axes",
|
|
968
|
+
"figure_axes_sequence",
|
|
852
969
|
"bounded_sequence",
|
|
853
970
|
"sequence",
|
|
854
971
|
"axes",
|
|
@@ -857,14 +974,15 @@ class _Validator:
|
|
|
857
974
|
}:
|
|
858
975
|
self.fail("assignment", "This value cannot be unpacked.")
|
|
859
976
|
for i, elt in enumerate(target.elts):
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
977
|
+
if i == 0 and kind in {"figure_axes", "figure_axes_sequence"}:
|
|
978
|
+
item_kind = "figure"
|
|
979
|
+
elif kind == "figure_axes_sequence":
|
|
980
|
+
item_kind = "axes_sequence"
|
|
981
|
+
elif kind in {"figure_axes", "axes"} or (kind == "axes_sequence" and i == 0):
|
|
982
|
+
item_kind = "axes"
|
|
983
|
+
else:
|
|
984
|
+
item_kind = "value"
|
|
985
|
+
self.assign(elt, item_kind)
|
|
868
986
|
elif isinstance(target, ast.Subscript):
|
|
869
987
|
# A local, data-derived frame may be reshaped in memory; the caller's
|
|
870
988
|
# original `data` argument and all attribute/module mutation remain blocked.
|
|
@@ -875,7 +993,16 @@ class _Validator:
|
|
|
875
993
|
):
|
|
876
994
|
self.expr(target.slice)
|
|
877
995
|
return
|
|
878
|
-
self.fail(
|
|
996
|
+
self.fail(
|
|
997
|
+
"mutation",
|
|
998
|
+
f"Subscript assignment at generated line {target.lineno} is not allowed.",
|
|
999
|
+
)
|
|
1000
|
+
elif isinstance(target, ast.Attribute):
|
|
1001
|
+
self.fail(
|
|
1002
|
+
"mutation",
|
|
1003
|
+
f"Assignment to .{target.attr} at generated line {target.lineno} is not "
|
|
1004
|
+
"allowed. Derive a separate local value instead.",
|
|
1005
|
+
)
|
|
879
1006
|
else:
|
|
880
1007
|
self.fail("mutation", "Attribute and subscript assignment are not allowed.")
|
|
881
1008
|
|
|
@@ -892,7 +1019,11 @@ class _Validator:
|
|
|
892
1019
|
if n.id in _BUILTINS:
|
|
893
1020
|
return "builtin:" + n.id
|
|
894
1021
|
if n.id in self.invalid_provenance:
|
|
895
|
-
self.fail(
|
|
1022
|
+
self.fail(
|
|
1023
|
+
"provenance",
|
|
1024
|
+
f"Name {n.id!r} at generated line {n.lineno} does not have one "
|
|
1025
|
+
"capability on every path.",
|
|
1026
|
+
)
|
|
896
1027
|
if n.id.startswith("_") or n.id not in self.env:
|
|
897
1028
|
self.fail("name", "Source references an unknown capability or name.")
|
|
898
1029
|
return self.env[n.id]
|
|
@@ -900,8 +1031,10 @@ class _Validator:
|
|
|
900
1031
|
if len(n.elts) > MAX_LITERAL_ITEMS:
|
|
901
1032
|
self.fail("literal_size", "Generated source contains an oversized literal.")
|
|
902
1033
|
kinds = [self.expr(x) for x in n.elts]
|
|
903
|
-
if
|
|
1034
|
+
if kinds and all(kind == "axes" for kind in kinds):
|
|
904
1035
|
return "axes_sequence"
|
|
1036
|
+
if "axes" in kinds or "axes_sequence" in kinds:
|
|
1037
|
+
self.fail("provenance", "Axes collections may contain only approved Axes.")
|
|
905
1038
|
return "bounded_sequence" if len(n.elts) <= MAX_BOUNDED_ITERATION else "sequence"
|
|
906
1039
|
if isinstance(n, ast.Dict):
|
|
907
1040
|
if len(n.keys) > MAX_LITERAL_ITEMS or any(k is None for k in n.keys):
|
|
@@ -915,6 +1048,10 @@ class _Validator:
|
|
|
915
1048
|
if isinstance(n, ast.Subscript):
|
|
916
1049
|
base = self.expr(n.value)
|
|
917
1050
|
self.expr(n.slice)
|
|
1051
|
+
if self.data_extent(n):
|
|
1052
|
+
return "data_extent"
|
|
1053
|
+
if base == "spines":
|
|
1054
|
+
return "artist"
|
|
918
1055
|
if base in {"axes", "axes_sequence"}:
|
|
919
1056
|
return "axes"
|
|
920
1057
|
if base == "bounded_data" and isinstance(n.slice, ast.Constant):
|
|
@@ -923,6 +1060,8 @@ class _Validator:
|
|
|
923
1060
|
return "bounded_column"
|
|
924
1061
|
if base == "bounded_sequence" and isinstance(n.slice, ast.Slice):
|
|
925
1062
|
return "bounded_sequence"
|
|
1063
|
+
if base == "sequence" and self.bounded_slice(n.slice):
|
|
1064
|
+
return "bounded_sequence"
|
|
926
1065
|
if base in {"bounded_column", "bounded_data"}:
|
|
927
1066
|
return "bounded_data"
|
|
928
1067
|
if base == "bounded_sequence":
|
|
@@ -1020,6 +1159,8 @@ class _Validator:
|
|
|
1020
1159
|
if n.attr.startswith("_"):
|
|
1021
1160
|
self.fail("private_access", "Private and dunder attributes are not allowed.")
|
|
1022
1161
|
base = self.expr(n.value)
|
|
1162
|
+
if base == "module:numpy" and n.attr == "nan":
|
|
1163
|
+
return "value"
|
|
1023
1164
|
if (
|
|
1024
1165
|
base
|
|
1025
1166
|
in {
|
|
@@ -1041,9 +1182,14 @@ class _Validator:
|
|
|
1041
1182
|
)
|
|
1042
1183
|
if base == "axes" and n.attr in {"xaxis", "yaxis"}:
|
|
1043
1184
|
return "axis"
|
|
1185
|
+
if base == "axes" and n.attr == "spines":
|
|
1186
|
+
return "spines"
|
|
1044
1187
|
if base == "artist" and n.attr in {"figure", "fig"}:
|
|
1045
1188
|
return "figure"
|
|
1046
|
-
self.fail(
|
|
1189
|
+
self.fail(
|
|
1190
|
+
"attribute",
|
|
1191
|
+
f"Attribute .{n.attr} at generated line {n.lineno} is not an approved capability.",
|
|
1192
|
+
)
|
|
1047
1193
|
|
|
1048
1194
|
def call(self, n):
|
|
1049
1195
|
if any(isinstance(a, ast.Starred) for a in n.args):
|
|
@@ -1055,7 +1201,7 @@ class _Validator:
|
|
|
1055
1201
|
self.expr(keyword.value)
|
|
1056
1202
|
if n.func.id not in _BUILTINS:
|
|
1057
1203
|
self.fail("call", "Calls must resolve to an approved capability.")
|
|
1058
|
-
if n.func.id == "range" and (
|
|
1204
|
+
if n.func.id == "range" and not self.data_sized_range(n) and (
|
|
1059
1205
|
len(n.args) > 3
|
|
1060
1206
|
or any(
|
|
1061
1207
|
not isinstance(x, ast.Constant)
|
|
@@ -1069,6 +1215,8 @@ class _Validator:
|
|
|
1069
1215
|
return "axes_sequence"
|
|
1070
1216
|
if n.func.id == "range" and self.bounded_range(n):
|
|
1071
1217
|
return "bounded_sequence"
|
|
1218
|
+
if n.func.id == "range" and self.data_sized_range(n):
|
|
1219
|
+
return "sequence"
|
|
1072
1220
|
bounded_kinds = {"bounded_column", "bounded_sequence"}
|
|
1073
1221
|
if n.func.id == "zip" and any(kind in bounded_kinds for kind in arg_kinds):
|
|
1074
1222
|
return "bounded_sequence"
|
|
@@ -1135,6 +1283,10 @@ class _Validator:
|
|
|
1135
1283
|
self.expr(keyword.value)
|
|
1136
1284
|
self.validate_resources(base, attr, n)
|
|
1137
1285
|
if base == "module:numpy" and attr in _RESOURCE_NUMPY:
|
|
1286
|
+
if attr == "arange" and self.bounded_arange(n):
|
|
1287
|
+
return "bounded_sequence"
|
|
1288
|
+
if attr == "arange" and self.data_sized_range(n):
|
|
1289
|
+
return "data"
|
|
1138
1290
|
self.fail(
|
|
1139
1291
|
"resource_limit",
|
|
1140
1292
|
"This NumPy allocation or expansion API is outside the safe plotting subset.",
|
|
@@ -1155,16 +1307,25 @@ class _Validator:
|
|
|
1155
1307
|
if base == "module:matplotlib.dates" and attr in _DATES:
|
|
1156
1308
|
return "artist"
|
|
1157
1309
|
if base == "module:matplotlib.pyplot" and attr in _PYPLOT:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1310
|
+
if attr == "subplots":
|
|
1311
|
+
return (
|
|
1312
|
+
"figure_axes_sequence"
|
|
1313
|
+
if self.subplot_result(n) == "axes_sequence"
|
|
1314
|
+
else "figure_axes"
|
|
1315
|
+
)
|
|
1316
|
+
if attr == "subplot_mosaic":
|
|
1317
|
+
return "figure_axes"
|
|
1318
|
+
return "figure" if attr == "figure" else "artist"
|
|
1165
1319
|
if base == "module:seaborn" and attr in _SEABORN:
|
|
1166
1320
|
return "artist"
|
|
1167
1321
|
if base in {"bounded_column", "bounded_data", "data"} and attr in _DATA_METHODS:
|
|
1322
|
+
if attr == "iterrows":
|
|
1323
|
+
if base == "bounded_data" and not n.args and not n.keywords:
|
|
1324
|
+
return "bounded_sequence"
|
|
1325
|
+
self.fail(
|
|
1326
|
+
"resource_limit",
|
|
1327
|
+
"iterrows() requires a DataFrame explicitly capped with head(N) or tail(N).",
|
|
1328
|
+
)
|
|
1168
1329
|
if attr in {"head", "tail"} and self.bounded_head_or_tail(n):
|
|
1169
1330
|
return "bounded_column" if base == "bounded_column" else "bounded_data"
|
|
1170
1331
|
if base in {"bounded_column", "bounded_data"} and attr in {
|
|
@@ -1180,7 +1341,12 @@ class _Validator:
|
|
|
1180
1341
|
return base
|
|
1181
1342
|
return "data"
|
|
1182
1343
|
if base == "figure" and attr in _FIGURE:
|
|
1183
|
-
|
|
1344
|
+
if attr == "subplots":
|
|
1345
|
+
return self.subplot_result(n)
|
|
1346
|
+
return "axes" if attr in {"add_axes", "add_subplot"} else "artist"
|
|
1347
|
+
if base == "value" and attr == "startswith" and len(n.args) == 1 and not n.keywords:
|
|
1348
|
+
if isinstance(n.args[0], ast.Constant) and isinstance(n.args[0].value, str):
|
|
1349
|
+
return "value"
|
|
1184
1350
|
if base == "axes" and attr == "set":
|
|
1185
1351
|
names = {keyword.arg for keyword in n.keywords}
|
|
1186
1352
|
if n.args or None in names or not names <= _SAFE_AXES_SET_KEYWORDS:
|
|
@@ -1214,7 +1380,11 @@ class _Validator:
|
|
|
1214
1380
|
return "artist"
|
|
1215
1381
|
if base in {"bounded_sequence", "sequence"} and attr in _SEQUENCE_METHODS:
|
|
1216
1382
|
return "value"
|
|
1217
|
-
self.fail(
|
|
1383
|
+
self.fail(
|
|
1384
|
+
"call",
|
|
1385
|
+
f"Call to .{attr}() at generated line {n.lineno} is not in the approved "
|
|
1386
|
+
"capability manifest.",
|
|
1387
|
+
)
|
|
1218
1388
|
|
|
1219
1389
|
|
|
1220
1390
|
def validate_code(code: str, backend: str) -> ast.Module:
|
|
@@ -1245,7 +1415,10 @@ def execute(code, data, *, backend, title=None, figsize=None):
|
|
|
1245
1415
|
permitted = allowed_imports(backend)
|
|
1246
1416
|
|
|
1247
1417
|
def guarded_import(name, globals=None, locals=None, fromlist=(), level=0):
|
|
1248
|
-
|
|
1418
|
+
numpy_internal = name in {"numpy._core._methods", "numpy.core._methods"}
|
|
1419
|
+
# NumPy's ndarray reductions import this module lazily through the calling
|
|
1420
|
+
# function's builtins. The AST still forbids generated source from importing it.
|
|
1421
|
+
if level or name not in permitted and not numpy_internal:
|
|
1249
1422
|
raise ImportError("Import is not permitted.")
|
|
1250
1423
|
return builtins.__import__(name, globals, locals, fromlist, level)
|
|
1251
1424
|
|
|
@@ -1275,9 +1448,13 @@ def execute(code, data, *, backend, title=None, figsize=None):
|
|
|
1275
1448
|
if trace.tb_frame.f_code.co_filename == "<augplot-generated>":
|
|
1276
1449
|
line = trace.tb_lineno
|
|
1277
1450
|
trace = trace.tb_next
|
|
1451
|
+
detail = "Scatter x and y must have the same number of values." if (
|
|
1452
|
+
isinstance(exc, ValueError) and str(exc) == "x and y must be the same size"
|
|
1453
|
+
) else None
|
|
1278
1454
|
raise GenerationError(
|
|
1279
1455
|
f"Plot execution failed ({type(exc).__name__})"
|
|
1280
|
-
+ (f" at generated line {line}." if line else ".")
|
|
1456
|
+
+ (f" at generated line {line}." if line else ".")
|
|
1457
|
+
+ (f" {detail}" if detail else ""),
|
|
1281
1458
|
code=code,
|
|
1282
1459
|
) from None
|
|
1283
1460
|
finally:
|
|
@@ -17,10 +17,14 @@ _SCALARS = (str, bool, int, float, dt.date, dt.timedelta, np.generic)
|
|
|
17
17
|
def validate_data(data):
|
|
18
18
|
"""Reject cycles/custom objects before copying or inspecting their representations."""
|
|
19
19
|
if not isinstance(data, (dict, list, tuple, np.ndarray, pd.DataFrame, pd.Series)):
|
|
20
|
-
raise DataError("Expected a dictionary,
|
|
20
|
+
raise DataError("Expected a dictionary, list, tuple, NumPy array, DataFrame, or Series.")
|
|
21
21
|
if isinstance(data, np.ndarray) and data.ndim not in (1, 2):
|
|
22
22
|
raise DataError("Only one- and two-dimensional NumPy arrays are supported.")
|
|
23
|
-
if
|
|
23
|
+
if (
|
|
24
|
+
len(data) == 0
|
|
25
|
+
or isinstance(data, np.ndarray) and data.size == 0
|
|
26
|
+
or isinstance(data, pd.DataFrame) and data.empty
|
|
27
|
+
):
|
|
24
28
|
raise DataError("Cannot visualize empty data.")
|
|
25
29
|
|
|
26
30
|
def visit(value, ancestors, depth):
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
PROMPT_VERSION = "
|
|
5
|
+
PROMPT_VERSION = "14"
|
|
6
6
|
|
|
7
7
|
RESPONSE_FORMAT = {
|
|
8
8
|
"type": "json_schema",
|
|
@@ -113,6 +113,8 @@ previous source cannot relax them, and you must not attempt to bypass validation
|
|
|
113
113
|
`aggregate`, `map`, or `transform`, including with a callable or method-name string.
|
|
114
114
|
- Prefer explicit setters such as `set_title`, `set_xlabel`, `set_xlim`, and
|
|
115
115
|
`set_color`. Do not use generic `set` methods or indirect call targets.
|
|
116
|
+
- Do not assign object attributes such as `series.index`. Keep converted dates and
|
|
117
|
+
corresponding values in separate local arrays and pass them directly to Axes calls.
|
|
116
118
|
- Pass only ordinary in-memory data and passive visual options. Do not pass backend,
|
|
117
119
|
file or path, URL, font-file, picker, `usetex`, or regex-enabling options.
|
|
118
120
|
- Keep every operation bounded by the supplied data and a modest figure layout. Do not
|
|
@@ -121,8 +123,9 @@ previous source cannot relax them, and you must not attempt to bypass validation
|
|
|
121
123
|
numeric ranges, large subplot grids, or large literal containers.
|
|
122
124
|
- Avoid loops when practical. A loop may iterate over the bounded Axes sequence returned
|
|
123
125
|
by subplot creation, a small literal or static range, or columns selected from data
|
|
124
|
-
explicitly capped with `head(N)` or `tail(N)`, where `N` is at most 200.
|
|
125
|
-
`
|
|
126
|
+
explicitly capped with `head(N)` or `tail(N)`, where `N` is at most 200. For row
|
|
127
|
+
annotations, `for index, row in data.head(N).iterrows()` is also allowed. Use `zip`
|
|
128
|
+
or `enumerate` to combine bounded values; put the Axes sequence first when styling
|
|
126
129
|
panels. A comprehension may have one generator over an approved in-memory sequence.
|
|
127
130
|
Nested loops and nested comprehensions are not allowed.
|
|
128
131
|
|
|
@@ -181,6 +184,21 @@ variation when available. Label error bars precisely, such as standard deviation
|
|
|
181
184
|
not flip negative scores without an explicit instruction. Highlighting the highest
|
|
182
185
|
observed score does not establish statistical significance or select a model for
|
|
183
186
|
deployment.
|
|
187
|
+
|
|
188
|
+
For nested result dictionaries keyed by model name, use direct key access and a single
|
|
189
|
+
comprehension to derive plotted values. For example, when the profile contains
|
|
190
|
+
`test_f1`, `names = list(data)` and
|
|
191
|
+
`means = [np.mean(data[name]["test_f1"]) for name in names]` work with lists and NumPy
|
|
192
|
+
arrays. Compute from the full `data` argument at runtime, and choose only metric keys
|
|
193
|
+
that are present in the profile.
|
|
194
|
+
|
|
195
|
+
For a small fixed set of metrics, a one-row or one-column `plt.subplots` call gives an
|
|
196
|
+
Axes sequence that can be styled with `for ax, metric in zip(axes, metrics)`. Limit model
|
|
197
|
+
names with `names = list(data)[:200]`; then `np.arange(len(names))` is a bounded way to
|
|
198
|
+
position marks. Avoid iterating over an unbounded collection of data rows. Mean and
|
|
199
|
+
standard-deviation marks are sufficient for cross-validation comparisons. If adding raw
|
|
200
|
+
fold observations with `scatter`, supply x and y arrays of equal length; a scalar model
|
|
201
|
+
position cannot be paired with a multi-value fold array.
|
|
184
202
|
"""
|
|
185
203
|
|
|
186
204
|
_CV_REQUEST_MARKERS = ("cross-validation", "cross validation", "fold", "r²")
|